mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-28 22:42:11 +01:00
📝 docs: update README documentation for 13 libraries
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# Library Reference Guide
|
||||
|
||||
> **Last Updated:** 2025-11-20
|
||||
> **Last Updated:** 2025-11-25
|
||||
> **Angular Version:** 20.3.6
|
||||
> **Nx Version:** 21.3.2
|
||||
> **Total Libraries:** 72
|
||||
@@ -37,7 +37,7 @@ A comprehensive checkout and shopping cart management library for Angular applic
|
||||
**Location:** `libs/checkout/data-access/`
|
||||
|
||||
### `@isa/checkout/feature/reward-order-confirmation`
|
||||
## Overview
|
||||
A feature library providing a comprehensive order confirmation page for reward orders with support for printing, address display, and loyalty reward collection.
|
||||
|
||||
**Location:** `libs/checkout/feature/reward-order-confirmation/`
|
||||
|
||||
@@ -120,22 +120,22 @@ A sophisticated tab management system for Angular applications providing browser
|
||||
**Location:** `libs/crm/data-access/`
|
||||
|
||||
### `@isa/crm/feature/customer-bon-redemption`
|
||||
This library was generated with [Nx](https://nx.dev).
|
||||
A feature library providing customer loyalty receipt (Bon) redemption functionality with validation and point allocation capabilities.
|
||||
|
||||
**Location:** `libs/crm/feature/customer-bon-redemption/`
|
||||
|
||||
### `@isa/crm/feature/customer-booking`
|
||||
This library was generated with [Nx](https://nx.dev).
|
||||
A standalone Angular component that enables customer loyalty card point booking and reversal with configurable booking reasons.
|
||||
|
||||
**Location:** `libs/crm/feature/customer-booking/`
|
||||
|
||||
### `@isa/crm/feature/customer-card-transactions`
|
||||
This library was generated with [Nx](https://nx.dev).
|
||||
A standalone Angular component that displays customer loyalty card transaction history with automatic data loading and refresh capabilities.
|
||||
|
||||
**Location:** `libs/crm/feature/customer-card-transactions/`
|
||||
|
||||
### `@isa/crm/feature/customer-loyalty-cards`
|
||||
This library was generated with [Nx](https://nx.dev).
|
||||
Customer loyalty cards feature module for displaying and managing customer bonus cards (Kundenkarten) with points tracking and card management actions.
|
||||
|
||||
**Location:** `libs/crm/feature/customer-loyalty-cards/`
|
||||
|
||||
@@ -144,7 +144,7 @@ This library was generated with [Nx](https://nx.dev).
|
||||
## Icons (1 library)
|
||||
|
||||
### `@isa/icons`
|
||||
## Overview
|
||||
A centralized icon library for the ISA-Frontend monorepo providing inline SVG icons as string constants.
|
||||
|
||||
**Location:** `libs/icons/`
|
||||
|
||||
@@ -256,7 +256,7 @@ Angular library for generating Code 128 barcodes using [JsBarcode](https://githu
|
||||
**Location:** `libs/shared/barcode/`
|
||||
|
||||
### `@isa/shared/delivery`
|
||||
This library was generated with [Nx](https://nx.dev).
|
||||
A reusable Angular component library for displaying order destination information with support for multiple delivery types (shipping, pickup, in-store, and digital downloads).
|
||||
|
||||
**Location:** `libs/shared/delivery/`
|
||||
|
||||
@@ -286,7 +286,7 @@ An accessible, feature-rich Angular quantity selector component with dropdown pr
|
||||
**Location:** `libs/shared/quantity-control/`
|
||||
|
||||
### `@isa/shared/scanner`
|
||||
## Overview
|
||||
Enterprise-grade barcode scanning library for ISA-Frontend using the Scandit SDK, providing mobile barcode scanning capabilities for iOS and Android platforms.
|
||||
|
||||
**Location:** `libs/shared/scanner/`
|
||||
|
||||
@@ -394,17 +394,17 @@ Lightweight Angular utility library for validating EAN (European Article Number)
|
||||
**Location:** `libs/utils/ean-validation/`
|
||||
|
||||
### `@isa/utils/format-name`
|
||||
This library was generated with [Nx](https://nx.dev).
|
||||
A utility library for consistently formatting person and organisation names across the ISA-Frontend application.
|
||||
|
||||
**Location:** `libs/utils/format-name/`
|
||||
|
||||
### `@isa/utils/positive-integer-input`
|
||||
This library was generated with [Nx](https://nx.dev).
|
||||
An Angular directive that ensures only positive integers can be entered into number input fields through keyboard blocking, paste sanitization, and input validation.
|
||||
|
||||
**Location:** `libs/utils/positive-integer-input/`
|
||||
|
||||
### `@isa/utils/scroll-position`
|
||||
## Overview
|
||||
Utility library providing scroll position restoration and scroll-to-top functionality for Angular applications.
|
||||
|
||||
**Location:** `libs/utils/scroll-position/`
|
||||
|
||||
|
||||
@@ -1,135 +1,249 @@
|
||||
# checkout-feature-reward-order-confirmation
|
||||
# @isa/checkout/feature/reward-order-confirmation
|
||||
|
||||
A feature library providing a comprehensive order confirmation page for reward orders with support for printing, address display, and loyalty reward collection.
|
||||
|
||||
## Overview
|
||||
|
||||
The `@isa/checkout/feature/reward-order-confirmation` library provides the **reward order confirmation screen** displayed after customers complete a loyalty reward redemption purchase. It shows a comprehensive summary of completed reward orders, including shipping/billing addresses, product details, loyalty points used, and delivery method information.
|
||||
This feature provides a complete order confirmation experience after a reward order has been placed. It displays order details grouped by delivery destination, shows payer and shipping information, and enables users with appropriate permissions to print order confirmations. For items with the 'Rücklage' (layaway) feature, it provides an action card that allows staff to collect loyalty rewards in various ways (collect, donate, or cancel).
|
||||
|
||||
**Type:** Routed feature library with lazy-loaded components
|
||||
The library uses NgRx Signal Store for reactive state management, integrating with the OMS (Order Management System) API to load and display order data. It supports multiple orders being displayed simultaneously by accepting comma-separated display order IDs in the route parameter.
|
||||
|
||||
## Features
|
||||
## Architecture
|
||||
|
||||
- **Multi-Order Support**: Display multiple orders from a single shopping cart session
|
||||
- **Order Type Awareness**: Conditional display based on delivery method (Delivery, Pickup, In-Store)
|
||||
- **Address Deduplication**: Smart handling of duplicate addresses across multiple orders
|
||||
- **Loyalty Points Display**: Shows loyalty points (Lesepunkte) used for each reward item
|
||||
- **Product Information**: Comprehensive product details including name, contributors, EAN, and quantity
|
||||
- **Destination Information**: Delivery/pickup destination details
|
||||
### State Management
|
||||
|
||||
## Main Components
|
||||
The library uses a **signalStore** (`OrderConfiramtionStore`) that:
|
||||
- Loads display orders via `DisplayOrdersResource`
|
||||
- Computes derived state (payers, shipping addresses, target branches)
|
||||
- Determines which order type features are present (delivery, pickup, in-store)
|
||||
- Provides reactive data to all child components
|
||||
|
||||
### RewardOrderConfirmationComponent (Main Container)
|
||||
- **Selector:** `checkout-reward-order-confirmation`
|
||||
- **Route:** `/:orderIds` (accepts multiple order IDs separated by `+`)
|
||||
- Orchestrates the entire confirmation view
|
||||
- Manages state via `OrderConfiramtionStore`
|
||||
### Route Configuration
|
||||
|
||||
The feature is accessible via the route pattern: `:displayOrderIds`
|
||||
|
||||
Example: `/order-confirmation/1234,5678` displays orders with display IDs 1234 and 5678.
|
||||
|
||||
The route includes:
|
||||
- OMS action handlers for command execution
|
||||
- Tab cleanup on deactivation
|
||||
- Lazy-loaded main component
|
||||
|
||||
## Components
|
||||
|
||||
### RewardOrderConfirmationComponent
|
||||
|
||||
**Selector:** `checkout-reward-order-confirmation`
|
||||
|
||||
Main container component that orchestrates the order confirmation page.
|
||||
|
||||
**Key Responsibilities:**
|
||||
- Parses display order IDs from route parameters (comma-separated)
|
||||
- Initializes the store with tab ID and order IDs
|
||||
- Triggers display order loading via `DisplayOrdersResource`
|
||||
- Composes child components (header, addresses, item list)
|
||||
|
||||
**Effects:**
|
||||
- Updates store state when route parameters or tab ID change
|
||||
- Loads display orders when order IDs change
|
||||
|
||||
### OrderConfirmationHeaderComponent
|
||||
- **Selector:** `checkout-order-confirmation-header`
|
||||
- Displays header: "Prämienausgabe abgeschlossen" (Reward distribution completed)
|
||||
|
||||
**Selector:** `checkout-order-confirmation-header`
|
||||
|
||||
Displays the page header with print functionality.
|
||||
|
||||
**Features:**
|
||||
- Print button that triggers `CheckoutPrintFacade.printOrderConfirmation()`
|
||||
- Role-based access control (print feature restricted by role)
|
||||
- Integrates with the ISA printing system
|
||||
|
||||
**Dependencies:**
|
||||
- `@isa/common/print` - Print button and printer selection
|
||||
- `@isa/core/auth` - Role-based directive (`*ifRole`)
|
||||
|
||||
### OrderConfirmationAddressesComponent
|
||||
- **Selector:** `checkout-order-confirmation-addresses`
|
||||
- Displays billing addresses, shipping addresses, and pickup branch information
|
||||
- Conditionally shows sections based on order type
|
||||
|
||||
**Selector:** `checkout-order-confirmation-addresses`
|
||||
|
||||
Displays payer and destination information based on order types.
|
||||
|
||||
**Displayed Information:**
|
||||
- **Payers:** Deduplicated list of buyers across all orders
|
||||
- **Shipping Addresses:** Shown only if orders have delivery features (Delivery, DigitalShipping, B2BShipping)
|
||||
- **Target Branches:** Shown only if orders have in-store features (InStore, Pickup)
|
||||
|
||||
**Dependencies:**
|
||||
- `@isa/shared/address` - Address display component
|
||||
- `@isa/crm/data-access` - Customer name formatting and address deduplication
|
||||
|
||||
### OrderConfirmationItemListComponent
|
||||
- **Selector:** `checkout-order-confirmation-item-list`
|
||||
- Displays order type badge with icon (Delivery/Pickup/In-Store)
|
||||
- Renders list of order items for each order
|
||||
|
||||
**Selector:** `checkout-order-confirmation-item-list`
|
||||
|
||||
Groups and displays order items by delivery destination and type.
|
||||
|
||||
**Key Features:**
|
||||
- Groups items using `groupItemsByDeliveryDestination()` helper
|
||||
- Displays delivery type icons (Versand, Rücklage, B2B Versand)
|
||||
- Optimized rendering with `trackBy` function for item groups
|
||||
|
||||
**Grouping Logic:**
|
||||
Items are grouped by:
|
||||
1. Order type (e.g., Delivery, Pickup, InStore)
|
||||
2. Shipping address (for delivery orders)
|
||||
3. Target branch (for pickup/in-store orders)
|
||||
|
||||
### OrderConfirmationItemListItemComponent
|
||||
- **Selector:** `checkout-order-confirmation-item-list-item`
|
||||
- Displays individual product information, quantity, loyalty points, and destination info
|
||||
|
||||
**Selector:** `checkout-order-confirmation-item-list-item`
|
||||
|
||||
Displays individual order item details within a group.
|
||||
|
||||
**Inputs:**
|
||||
- `item` (required): `DisplayOrderItemDTO` - The order item to display
|
||||
- `order` (required): `OrderItemGroup` - The group containing delivery type and destination
|
||||
|
||||
**Features:**
|
||||
- Product information display via `ProductInfoComponent`
|
||||
- Destination information via `DisplayOrderDestinationInfoComponent`
|
||||
- Action card for loyalty reward collection (conditional)
|
||||
- Loyalty points display
|
||||
|
||||
### ConfirmationListItemActionCardComponent
|
||||
- **Selector:** `checkout-confirmation-list-item-action-card`
|
||||
- Placeholder component for future action functionality
|
||||
|
||||
## State Management
|
||||
**Selector:** `checkout-confirmation-list-item-action-card`
|
||||
|
||||
**OrderConfiramtionStore** (NgRx Signals)
|
||||
Provides loyalty reward collection interface for layaway items.
|
||||
|
||||
**State:**
|
||||
- `tabId`: Current tab identifier
|
||||
- `orderIds`: Array of order IDs to display
|
||||
**Inputs:**
|
||||
- `item` (required): `DisplayOrderItem` - The order item with loyalty information
|
||||
|
||||
**Computed Properties:**
|
||||
- `orders`: Filtered display orders from OMS metadata service
|
||||
- `shoppingCart`: Associated completed shopping cart
|
||||
- `payers`: Deduplicated billing addressees
|
||||
- `shippingAddresses`: Deduplicated shipping addressees
|
||||
- `targetBranches`: Deduplicated pickup branches
|
||||
- `hasDeliveryOrderTypeFeature`: Boolean indicating delivery orders
|
||||
- `hasTargetBranchFeature`: Boolean indicating pickup/in-store orders
|
||||
**Display Conditions:**
|
||||
The action card is displayed when ALL of the following are met:
|
||||
- Item has the 'Rücklage' (layaway) order type feature
|
||||
- AND either:
|
||||
- Item has a loyalty collect command available, OR
|
||||
- Item processing is already complete
|
||||
|
||||
**Features:**
|
||||
- **Action Selection:** Dropdown to choose collection type:
|
||||
- `Collect` - Collect loyalty points for customer
|
||||
- `Donate` - Donate points to charity
|
||||
- `Cancel` - Cancel the reward collection
|
||||
- **Processing States:** Shows current processing status (Ordered, InProgress, Complete)
|
||||
- **Command Execution:** Integrates with OMS command system for reward collection
|
||||
- **Loading States:** Displays loading indicators during API calls
|
||||
- **Completion Status:** Shows check icon when processing is complete
|
||||
|
||||
**Role-Based Access:** Collection functionality is restricted by role permissions.
|
||||
|
||||
**Dependencies:**
|
||||
- `@isa/oms/data-access` - Order reward collection facade and command handling
|
||||
- `@isa/ui/buttons` - Button and dropdown components
|
||||
- `@isa/checkout/data-access` - Order type feature detection and item utilities
|
||||
|
||||
## Routes
|
||||
|
||||
```typescript
|
||||
export const routes: Routes = [
|
||||
{
|
||||
path: ':displayOrderIds',
|
||||
loadComponent: () => import('./reward-order-confirmation.component'),
|
||||
canDeactivate: [canDeactivateTabCleanup],
|
||||
providers: [CoreCommandModule.forChild(OMS_ACTION_HANDLERS)]
|
||||
}
|
||||
];
|
||||
```
|
||||
|
||||
**Route Parameters:**
|
||||
- `displayOrderIds` - Comma-separated list of display order IDs (e.g., "1234,5678")
|
||||
|
||||
## Usage
|
||||
|
||||
### Routing Integration
|
||||
### Importing Routes
|
||||
|
||||
```typescript
|
||||
// In parent routing module
|
||||
{
|
||||
path: 'reward-confirmation',
|
||||
loadChildren: () => import('@isa/checkout/feature/reward-order-confirmation')
|
||||
.then(m => m.routes)
|
||||
}
|
||||
import { routes as rewardOrderConfirmationRoutes } from '@isa/checkout/feature/reward-order-confirmation';
|
||||
|
||||
const appRoutes: Routes = [
|
||||
{
|
||||
path: 'order-confirmation',
|
||||
children: rewardOrderConfirmationRoutes
|
||||
}
|
||||
];
|
||||
```
|
||||
|
||||
### URL Structure
|
||||
### Navigation Example
|
||||
|
||||
```
|
||||
/reward-confirmation/123+456+789
|
||||
```
|
||||
```typescript
|
||||
// Navigate to confirmation for single order
|
||||
router.navigate(['/order-confirmation', '1234']);
|
||||
|
||||
This displays confirmation for orders with IDs 123, 456, and 789.
|
||||
// Navigate to confirmation for multiple orders
|
||||
router.navigate(['/order-confirmation', '1234,5678,9012']);
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
**Data Access:**
|
||||
- `@isa/oms/data-access` - Order metadata and models
|
||||
- `@isa/checkout/data-access` - Checkout metadata and order type utilities
|
||||
- `@isa/crm/data-access` - Address deduplication utilities
|
||||
### Core Angular & NgRx
|
||||
- `@angular/core` - Angular framework
|
||||
- `@angular/router` - Routing
|
||||
- `@ngrx/signals` - Signal-based state management
|
||||
|
||||
**Core:**
|
||||
- `@isa/core/tabs` - Tab context management
|
||||
### ISA Libraries
|
||||
|
||||
**Shared Components:**
|
||||
- `@isa/shared/address` - Address rendering
|
||||
- `@isa/checkout/shared/product-info` - Product and destination information
|
||||
#### Data Access
|
||||
- `@isa/checkout/data-access` - Order type features, grouping utilities
|
||||
- `@isa/oms/data-access` - Display orders resource, reward collection, command handling
|
||||
- `@isa/crm/data-access` - Customer name formatting, address/branch deduplication
|
||||
|
||||
**UI:**
|
||||
- `@isa/icons` - Delivery method icons
|
||||
#### Shared Components
|
||||
- `@isa/checkout/shared/product-info` - Product display components
|
||||
- `@isa/shared/address` - Address display component
|
||||
- `@isa/common/print` - Print button and printer integration
|
||||
- `@isa/ui/buttons` - Button components
|
||||
- `@isa/ui/input-controls` - Dropdown components
|
||||
|
||||
## Data Flow
|
||||
#### Core Services
|
||||
- `@isa/core/tabs` - Tab service and cleanup guard
|
||||
- `@isa/core/auth` - Role-based access control
|
||||
- `@isa/core/command` - Command module integration
|
||||
|
||||
1. Route parameter (`orderIds`) is parsed into array of integers
|
||||
2. Store receives `tabId` from `TabService` and `orderIds` from route
|
||||
3. Store fetches orders from `OmsMetadataService` filtered by IDs
|
||||
4. Store fetches completed shopping cart from `CheckoutMetadataService`
|
||||
5. Components consume computed properties from store (addresses, order items, points)
|
||||
#### Icons
|
||||
- `@isa/icons` - ISA custom icon set
|
||||
- `@ng-icons/core` - Icon component
|
||||
|
||||
## Order Type Support
|
||||
|
||||
The library supports three delivery methods with specific icons:
|
||||
|
||||
- **Delivery** (`isaDeliveryVersand`): Shows shipping addresses
|
||||
- **Pickup** (`isaDeliveryRuecklage2`): Shows pickup branch
|
||||
- **In-Store** (`isaDeliveryRuecklage1`): Shows store branch
|
||||
### Generated APIs
|
||||
- `@generated/swagger/oms-api` - OMS API types (DisplayOrderItemDTO)
|
||||
|
||||
## Testing
|
||||
|
||||
Run tests with Vitest:
|
||||
The library uses Vitest for testing with comprehensive coverage of:
|
||||
- Component rendering and interactions
|
||||
- State management and computed signals
|
||||
- Route parameter parsing
|
||||
- Reward collection workflows
|
||||
- Conditional display logic
|
||||
|
||||
Run tests:
|
||||
```bash
|
||||
npx nx test checkout-feature-reward-order-confirmation --skip-nx-cache
|
||||
nx test checkout-feature-reward-order-confirmation
|
||||
```
|
||||
|
||||
**Test Framework:** Vitest
|
||||
**Coverage Output:** `coverage/libs/checkout/feature/reward-order-confirmation`
|
||||
## Key Features Summary
|
||||
|
||||
## Architecture Notes
|
||||
1. **Multi-Order Support:** Display confirmation for multiple orders simultaneously
|
||||
2. **Smart Grouping:** Items grouped by delivery destination for clarity
|
||||
3. **Conditional Displays:** Address sections shown based on order type features
|
||||
4. **Print Integration:** Role-based printing with printer selection
|
||||
5. **Loyalty Rewards:** In-page reward collection for layaway items
|
||||
6. **Reactive State:** Signal-based state management with computed values
|
||||
7. **Tab Integration:** Proper cleanup on tab deactivation
|
||||
8. **Role-Based Access:** Permissions enforced for sensitive operations
|
||||
|
||||
- **Component Prefix:** `checkout`
|
||||
- **Change Detection:** All components use `OnPush` strategy
|
||||
- **Standalone Architecture:** All components are standalone with explicit imports
|
||||
- **Project Name:** `checkout-feature-reward-order-confirmation`
|
||||
## Related Libraries
|
||||
|
||||
- `@isa/checkout/feature/checkout-process` - Main checkout flow
|
||||
- `@isa/oms/data-access` - Order management system integration
|
||||
- `@isa/checkout/data-access` - Checkout domain logic and utilities
|
||||
|
||||
@@ -1,205 +1,310 @@
|
||||
# Reward Selection Dialog
|
||||
# @isa/checkout/shared/reward-selection-dialog
|
||||
|
||||
Angular library for managing reward selection in shopping cart context. Allows users to toggle between regular purchase and reward redemption using bonus points.
|
||||
A comprehensive Angular dialog library for managing reward selection in the checkout process, allowing customers to allocate cart items between regular purchases (paid with money) and reward redemptions (paid with loyalty points).
|
||||
|
||||
## Features
|
||||
## Overview
|
||||
|
||||
- 🎯 Pre-built trigger component or direct service integration
|
||||
- 🔄 Automatic resource management (carts, bonus cards)
|
||||
- 📊 Smart grouping by order type and branch
|
||||
- 💾 NgRx Signals state management
|
||||
- ✅ Full TypeScript support
|
||||
This library provides a sophisticated dialog system that enables customers to decide how they want to purchase items that are eligible for both regular checkout and reward redemption. The dialog presents items from both the regular shopping cart and reward shopping cart, allowing customers to allocate quantities between payment methods.
|
||||
|
||||
The library includes:
|
||||
- A main dialog component with quantity allocation interface
|
||||
- A trigger component for opening the dialog from various contexts
|
||||
- State management using NgRx Signal Store
|
||||
- Services for managing dialog lifecycle and popup behavior
|
||||
- Automatic resource loading and validation
|
||||
|
||||
## Installation
|
||||
|
||||
```typescript
|
||||
```ts
|
||||
import {
|
||||
RewardSelectionDialogComponent,
|
||||
RewardSelectionService,
|
||||
RewardSelectionPopUpService,
|
||||
RewardSelectionTriggerComponent,
|
||||
RewardSelectionTriggerComponent
|
||||
} from '@isa/checkout/shared/reward-selection-dialog';
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
## Components
|
||||
|
||||
### Using the Trigger Component (Recommended)
|
||||
### RewardSelectionDialogComponent
|
||||
|
||||
Simplest integration - includes all providers automatically:
|
||||
The main dialog component that displays eligible items and allows customers to allocate quantities between regular cart and reward cart.
|
||||
|
||||
```typescript
|
||||
**Features:**
|
||||
- Displays customer's available loyalty points
|
||||
- Shows item grouping by order type (delivery, pickup, etc.) and branch
|
||||
- Real-time calculation of total price and loyalty points needed
|
||||
- Validates sufficient loyalty points before allowing save
|
||||
- Integrates with shopping cart resources
|
||||
|
||||
### RewardSelectionTriggerComponent
|
||||
|
||||
A button component that can be embedded in the UI to trigger the reward selection dialog.
|
||||
|
||||
**Features:**
|
||||
- Automatically shows/hides based on eligibility
|
||||
- Skeleton loader during resource loading
|
||||
- Handles dialog result and navigation
|
||||
- Provides feedback after selection
|
||||
|
||||
## API Reference
|
||||
|
||||
### Dialog Data Interface
|
||||
|
||||
```ts
|
||||
export type RewardSelectionDialogData = {
|
||||
rewardSelectionItems: RewardSelectionItem[];
|
||||
customerRewardPoints: number;
|
||||
closeText: string;
|
||||
};
|
||||
|
||||
export type RewardSelectionDialogResult =
|
||||
| {
|
||||
rewardSelectionItems: RewardSelectionItem[];
|
||||
}
|
||||
| undefined;
|
||||
```
|
||||
|
||||
**RewardSelectionDialogData:**
|
||||
- `rewardSelectionItems`: Array of items eligible for reward selection with current cart/reward quantities
|
||||
- `customerRewardPoints`: Total loyalty points available to the customer
|
||||
- `closeText`: Text for the cancel/close button
|
||||
|
||||
**RewardSelectionDialogResult:**
|
||||
- Returns updated `rewardSelectionItems` array if customer saves changes
|
||||
- Returns `undefined` if dialog is cancelled or no changes made
|
||||
|
||||
### Opening the Dialog
|
||||
|
||||
#### Using RewardSelectionService
|
||||
|
||||
```ts
|
||||
import { inject } from '@angular/core';
|
||||
import { RewardSelectionService } from '@isa/checkout/shared/reward-selection-dialog';
|
||||
|
||||
export class MyComponent {
|
||||
#rewardSelectionService = inject(RewardSelectionService);
|
||||
|
||||
async openDialog() {
|
||||
// Check if dialog can be opened (has eligible items and customer has points)
|
||||
if (this.#rewardSelectionService.canOpen()) {
|
||||
const result = await this.#rewardSelectionService.open({
|
||||
closeText: 'Abbrechen'
|
||||
});
|
||||
|
||||
if (result) {
|
||||
// Handle the result
|
||||
console.log('Updated items:', result.rewardSelectionItems);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Using RewardSelectionPopUpService
|
||||
|
||||
For automatic popup behavior (e.g., showing dialog once per session):
|
||||
|
||||
```ts
|
||||
import { inject } from '@angular/core';
|
||||
import {
|
||||
RewardSelectionPopUpService,
|
||||
NavigateAfterRewardSelection
|
||||
} from '@isa/checkout/shared/reward-selection-dialog';
|
||||
|
||||
export class CheckoutComponent {
|
||||
#popUpService = inject(RewardSelectionPopUpService);
|
||||
|
||||
async showPopupIfNeeded() {
|
||||
const navigation = await this.#popUpService.popUp();
|
||||
|
||||
switch (navigation) {
|
||||
case NavigateAfterRewardSelection.CART:
|
||||
// Navigate to regular shopping cart
|
||||
break;
|
||||
case NavigateAfterRewardSelection.REWARD:
|
||||
// Navigate to reward checkout
|
||||
break;
|
||||
case NavigateAfterRewardSelection.CATALOG:
|
||||
// Navigate back to catalog
|
||||
break;
|
||||
case undefined:
|
||||
// Stay on current page
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Using RewardSelectionTriggerComponent
|
||||
|
||||
Add the trigger component directly to your template:
|
||||
|
||||
```html
|
||||
<lib-reward-selection-trigger />
|
||||
```
|
||||
|
||||
The trigger component:
|
||||
- Automatically determines eligibility
|
||||
- Shows loading skeleton during resource fetching
|
||||
- Opens dialog on click
|
||||
- Handles navigation after selection
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Basic Dialog Integration
|
||||
|
||||
```ts
|
||||
import { Component, inject } from '@angular/core';
|
||||
import { RewardSelectionService } from '@isa/checkout/shared/reward-selection-dialog';
|
||||
|
||||
@Component({
|
||||
selector: 'app-checkout',
|
||||
template: `
|
||||
<button (click)="openRewardSelection()">
|
||||
Select Reward Items
|
||||
</button>
|
||||
`,
|
||||
providers: [RewardSelectionService]
|
||||
})
|
||||
export class CheckoutComponent {
|
||||
#rewardSelectionService = inject(RewardSelectionService);
|
||||
|
||||
async openRewardSelection() {
|
||||
await this.#rewardSelectionService.reloadResources();
|
||||
|
||||
if (this.#rewardSelectionService.canOpen()) {
|
||||
const result = await this.#rewardSelectionService.open({
|
||||
closeText: 'Cancel'
|
||||
});
|
||||
|
||||
if (result?.rewardSelectionItems.length) {
|
||||
// Process updated selections
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Using Trigger Component
|
||||
|
||||
```ts
|
||||
import { Component } from '@angular/core';
|
||||
import { RewardSelectionTriggerComponent } from '@isa/checkout/shared/reward-selection-dialog';
|
||||
|
||||
@Component({
|
||||
selector: 'app-checkout',
|
||||
template: `<lib-reward-selection-trigger />`,
|
||||
imports: [RewardSelectionTriggerComponent],
|
||||
selector: 'app-cart-summary',
|
||||
template: `
|
||||
<div class="cart-actions">
|
||||
<lib-reward-selection-trigger />
|
||||
<button>Proceed to Checkout</button>
|
||||
</div>
|
||||
`,
|
||||
imports: [RewardSelectionTriggerComponent]
|
||||
})
|
||||
export class CheckoutComponent {}
|
||||
export class CartSummaryComponent {}
|
||||
```
|
||||
|
||||
### Using the Pop-Up Service
|
||||
### One-Time Popup on Checkout Entry
|
||||
|
||||
More control over navigation flow:
|
||||
|
||||
```typescript
|
||||
import { Component, inject } from '@angular/core';
|
||||
```ts
|
||||
import { Component, inject, OnInit } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import {
|
||||
RewardSelectionPopUpService,
|
||||
NavigateAfterRewardSelection,
|
||||
RewardSelectionService,
|
||||
NavigateAfterRewardSelection
|
||||
} from '@isa/checkout/shared/reward-selection-dialog';
|
||||
import {
|
||||
SelectedShoppingCartResource,
|
||||
SelectedRewardShoppingCartResource,
|
||||
} from '@isa/checkout/data-access';
|
||||
|
||||
@Component({
|
||||
selector: 'app-custom-checkout',
|
||||
template: `<button (click)="openRewardSelection()">Select Rewards</button>`,
|
||||
providers: [
|
||||
// Required providers
|
||||
SelectedShoppingCartResource,
|
||||
RewardSelectionService,
|
||||
RewardSelectionPopUpService,
|
||||
],
|
||||
selector: 'app-checkout-entry',
|
||||
template: `<p>Loading checkout...</p>`,
|
||||
providers: [RewardSelectionPopUpService]
|
||||
})
|
||||
export class CustomCheckoutComponent {
|
||||
export class CheckoutEntryComponent implements OnInit {
|
||||
#router = inject(Router);
|
||||
#popUpService = inject(RewardSelectionPopUpService);
|
||||
|
||||
async openRewardSelection() {
|
||||
async ngOnInit() {
|
||||
const result = await this.#popUpService.popUp();
|
||||
|
||||
// Handle navigation: 'cart' | 'reward' | 'catalog' | undefined
|
||||
|
||||
if (result === NavigateAfterRewardSelection.CART) {
|
||||
// Navigate to cart
|
||||
await this.#router.navigate(['/cart']);
|
||||
} else if (result === NavigateAfterRewardSelection.REWARD) {
|
||||
await this.#router.navigate(['/reward-checkout']);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Using the Service Directly
|
||||
### Checking Eligibility
|
||||
|
||||
For custom UI or advanced use cases:
|
||||
|
||||
```typescript
|
||||
import { Component, inject } from '@angular/core';
|
||||
```ts
|
||||
import { Component, inject, computed } from '@angular/core';
|
||||
import { RewardSelectionService } from '@isa/checkout/shared/reward-selection-dialog';
|
||||
import {
|
||||
SelectedShoppingCartResource,
|
||||
} from '@isa/checkout/data-access';
|
||||
|
||||
@Component({
|
||||
selector: 'app-advanced',
|
||||
selector: 'app-cart',
|
||||
template: `
|
||||
@if (canOpen()) {
|
||||
<button (click)="openDialog()" [disabled]="isLoading()">
|
||||
{{ eligibleItemsCount() }} items as rewards ({{ availablePoints() }} points)
|
||||
</button>
|
||||
@if (hasRewardEligibleItems()) {
|
||||
<div class="reward-banner">
|
||||
You have items eligible for reward redemption!
|
||||
<button (click)="openDialog()">Choose Payment Method</button>
|
||||
</div>
|
||||
}
|
||||
`,
|
||||
providers: [
|
||||
SelectedShoppingCartResource,
|
||||
RewardSelectionService,
|
||||
],
|
||||
providers: [RewardSelectionService]
|
||||
})
|
||||
export class AdvancedComponent {
|
||||
#service = inject(RewardSelectionService);
|
||||
export class CartComponent {
|
||||
#rewardSelectionService = inject(RewardSelectionService);
|
||||
|
||||
canOpen = this.#service.canOpen;
|
||||
isLoading = this.#service.isLoading;
|
||||
eligibleItemsCount = computed(() => this.#service.eligibleItems().length);
|
||||
availablePoints = this.#service.primaryBonusCardPoints;
|
||||
hasRewardEligibleItems = this.#rewardSelectionService.canOpen;
|
||||
|
||||
async openDialog() {
|
||||
const result = await this.#service.open({ closeText: 'Cancel' });
|
||||
if (result) {
|
||||
// Handle result.rewardSelectionItems
|
||||
await this.#service.reloadResources();
|
||||
}
|
||||
const result = await this.#rewardSelectionService.open({
|
||||
closeText: 'Cancel'
|
||||
});
|
||||
// Handle result...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
## State Management
|
||||
|
||||
### RewardSelectionService
|
||||
The library uses `RewardSelectionStore` (NgRx Signal Store) internally to manage:
|
||||
|
||||
**Key Signals:**
|
||||
- `canOpen()`: `boolean` - Can dialog be opened
|
||||
- `isLoading()`: `boolean` - Loading state
|
||||
- `eligibleItems()`: `RewardSelectionItem[]` - Items available as rewards
|
||||
- `primaryBonusCardPoints()`: `number` - Available points
|
||||
- **State:** Item allocations, customer loyalty points
|
||||
- **Computed Values:** Total price, total loyalty points needed
|
||||
- **Methods:** Update cart quantities, update reward cart quantities
|
||||
|
||||
**Methods:**
|
||||
- `open({ closeText }): Promise<RewardSelectionDialogResult>` - Opens dialog
|
||||
- `reloadResources(): Promise<void>` - Reloads all data
|
||||
The store is scoped to each dialog instance and automatically initialized with dialog data.
|
||||
|
||||
### RewardSelectionPopUpService
|
||||
## Validation
|
||||
|
||||
**Methods:**
|
||||
- `popUp(): Promise<NavigateAfterRewardSelection | undefined>` - Opens dialog with navigation flow
|
||||
|
||||
**Return values:**
|
||||
- `'cart'` - Navigate to shopping cart
|
||||
- `'reward'` - Navigate to reward checkout
|
||||
- `'catalog'` - Navigate to catalog
|
||||
- `undefined` - No navigation needed
|
||||
|
||||
### Types
|
||||
|
||||
```typescript
|
||||
interface RewardSelectionItem {
|
||||
item: ShoppingCartItem;
|
||||
catalogPrice: Price | undefined;
|
||||
availabilityPrice: Price | undefined;
|
||||
catalogRewardPoints: number | undefined;
|
||||
cartQuantity: number;
|
||||
rewardCartQuantity: number;
|
||||
}
|
||||
|
||||
type RewardSelectionDialogResult = {
|
||||
rewardSelectionItems: RewardSelectionItem[];
|
||||
} | undefined;
|
||||
|
||||
type NavigateAfterRewardSelection = 'cart' | 'reward' | 'catalog';
|
||||
```
|
||||
|
||||
## Required Providers
|
||||
|
||||
When using `RewardSelectionService` or `RewardSelectionPopUpService` directly, provide:
|
||||
|
||||
```typescript
|
||||
providers: [
|
||||
SelectedShoppingCartResource, // Regular cart data
|
||||
RewardSelectionService, // Core service
|
||||
RewardSelectionPopUpService, // Optional: only if using pop-up
|
||||
]
|
||||
```
|
||||
|
||||
**Note:** `RewardSelectionTriggerComponent` includes all required providers automatically.
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
nx test reward-selection-dialog
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
reward-selection-dialog/
|
||||
├── helper/ # Pure utility functions
|
||||
├── resource/ # Data resources
|
||||
├── service/ # Business logic
|
||||
├── store/ # NgRx Signals state
|
||||
└── trigger/ # Trigger component
|
||||
```
|
||||
The dialog automatically validates:
|
||||
- Customer has sufficient loyalty points for selected reward items
|
||||
- Items are eligible for reward redemption (have loyalty points configured)
|
||||
- Changes were made before saving (prevents unnecessary API calls)
|
||||
|
||||
## Dependencies
|
||||
|
||||
- `@isa/checkout/data-access` - Cart resources
|
||||
- `@isa/crm/data-access` - Customer data
|
||||
- `@isa/catalogue/data-access` - Product catalog
|
||||
- `@isa/ui/dialog` - Dialog infrastructure
|
||||
- `@ngrx/signals` - State management
|
||||
Key dependencies on other @isa libraries:
|
||||
|
||||
- **@isa/checkout/data-access**: Shopping cart resources, reward selection models and facades
|
||||
- **@isa/ui/dialog**: Dialog infrastructure and directives
|
||||
- **@isa/ui/buttons**: Button components
|
||||
- **@isa/core/tabs**: Tab ID management for multi-tab support
|
||||
- **@isa/crm/data-access**: Customer card resource for loyalty points
|
||||
- **@isa/icons**: Order type icons for grouping display
|
||||
- **@ngrx/signals**: Signal Store for state management
|
||||
|
||||
## Architecture
|
||||
|
||||
The library follows a layered architecture:
|
||||
|
||||
1. **Presentation Layer**: Dialog and trigger components
|
||||
2. **State Layer**: Signal Store for reactive state management
|
||||
3. **Service Layer**: Dialog lifecycle and popup behavior services
|
||||
4. **Resource Layer**: Price and redemption points loading
|
||||
5. **Integration Layer**: Shopping cart and customer card resources
|
||||
|
||||
This separation ensures the dialog can be used in various contexts (manual trigger, automatic popup, embedded component) while maintaining consistent behavior and state management.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,217 @@
|
||||
# crm-feature-customer-bon-redemption
|
||||
# @isa/crm/feature/customer-bon-redemption
|
||||
|
||||
This library was generated with [Nx](https://nx.dev).
|
||||
A feature library providing customer loyalty receipt (Bon) redemption functionality with validation and point allocation capabilities.
|
||||
|
||||
## Running unit tests
|
||||
## Overview
|
||||
|
||||
Run `nx test crm-feature-customer-bon-redemption` to execute the unit tests.
|
||||
This library implements a complete workflow for redeeming customer receipts (Bons) for loyalty points. It provides a user interface that allows staff to enter a receipt number, validate it against the backend system, view receipt details, and process the redemption to add points to the customer's loyalty account.
|
||||
|
||||
The feature is designed for use in CRM contexts where customers need to receive loyalty points for receipts that were not automatically processed at the point of sale.
|
||||
|
||||
## Installation
|
||||
|
||||
```ts
|
||||
import { CrmFeatureCustomerBonRedemptionComponent } from '@isa/crm/feature/customer-bon-redemption';
|
||||
```
|
||||
|
||||
## Main Component
|
||||
|
||||
### CrmFeatureCustomerBonRedemptionComponent
|
||||
|
||||
The main component orchestrating the entire Bon redemption workflow.
|
||||
|
||||
**Selector:** `crm-customer-bon-redemption`
|
||||
|
||||
**Inputs:**
|
||||
- `cardCode?: string` - The customer's active loyalty card code (required for validation and redemption)
|
||||
|
||||
**Outputs:**
|
||||
- `redeemed: void` - Emitted when a Bon has been successfully redeemed
|
||||
|
||||
**Example Usage:**
|
||||
```html
|
||||
<crm-customer-bon-redemption
|
||||
[cardCode]="activeCardCode()"
|
||||
(redeemed)="onBonRedeemed()"
|
||||
/>
|
||||
```
|
||||
|
||||
**Features:**
|
||||
1. Input field for entering Bon number with validation
|
||||
2. Real-time Bon validation against backend API
|
||||
3. Display of validated Bon details (date, total amount)
|
||||
4. Redemption action with loading states and error handling
|
||||
5. Automatic form reset after successful redemption
|
||||
6. Integrated feedback dialogs for success/error states
|
||||
|
||||
## Child Components
|
||||
|
||||
### BonInputFieldComponent
|
||||
|
||||
Smart component managing the Bon number input field with validation controls.
|
||||
|
||||
**Selector:** `crm-bon-input-field`
|
||||
|
||||
**Inputs:**
|
||||
- `cardCode?: string` - Customer's loyalty card code
|
||||
|
||||
**Outputs:**
|
||||
- `validate: void` - Emitted when validation should be triggered
|
||||
- `clearForm: void` - Emitted when form should be cleared
|
||||
|
||||
**Features:**
|
||||
- Text input with required validation
|
||||
- "Bon suchen" (Search) button with loading state
|
||||
- Clear button when text is entered
|
||||
- Success/error message display
|
||||
- Enter key support for quick validation
|
||||
- Auto-focus on reset
|
||||
|
||||
### BonDetailsDisplayComponent
|
||||
|
||||
Displays validated Bon details including date and total amount.
|
||||
|
||||
**Selector:** `crm-bon-details-display`
|
||||
|
||||
**Features:**
|
||||
- Formatted date display
|
||||
- Total amount with currency formatting (German locale)
|
||||
- Conditional rendering (only visible when Bon is validated)
|
||||
|
||||
### BonRedemptionButtonComponent
|
||||
|
||||
Action button for processing the Bon redemption.
|
||||
|
||||
**Selector:** `crm-bon-redemption-button`
|
||||
|
||||
**Inputs:**
|
||||
- `disabled: boolean` - Whether the button should be disabled
|
||||
|
||||
**Outputs:**
|
||||
- `redeem: void` - Emitted when redemption is triggered
|
||||
|
||||
**Features:**
|
||||
- Primary action button with "Für Kunden verbuchen" label
|
||||
- Loading state during redemption process
|
||||
- Disabled state management
|
||||
|
||||
## State Management
|
||||
|
||||
The library uses `BonRedemptionStore` (from `@isa/crm/data-access`) for centralized state management:
|
||||
|
||||
**State Properties:**
|
||||
- `bonNumber: string` - Current Bon number input
|
||||
- `validatedBon?: ValidatedBon` - Validated Bon data (number, date, total)
|
||||
- `errorMessage?: string` - Current error message
|
||||
- `isValidating: boolean` - Validation loading state
|
||||
- `isRedeeming: boolean` - Redemption loading state
|
||||
- `disableRedemption: boolean` - Computed disable state
|
||||
|
||||
**Store Methods:**
|
||||
- `setBonNumber(value: string)` - Update Bon number
|
||||
- `setValidatedBon(bon: ValidatedBon)` - Store validated Bon data
|
||||
- `setError(message: string)` - Set error state
|
||||
- `setValidating(loading: boolean)` - Update validation loading state
|
||||
- `setRedeeming(loading: boolean)` - Update redemption loading state
|
||||
- `reset()` - Reset to initial state
|
||||
|
||||
## API Integration
|
||||
|
||||
### CustomerBonCheckResource
|
||||
|
||||
Uses Angular's Resource API for reactive Bon validation:
|
||||
|
||||
**Parameters:**
|
||||
- `cardCode: string` - Customer's loyalty card code
|
||||
- `bonNr: string` - Bon number to validate
|
||||
|
||||
**Returns:** Validated Bon data with date and total amount
|
||||
|
||||
### CustomerBonRedemptionFacade
|
||||
|
||||
Handles the actual Bon redemption operation:
|
||||
|
||||
**Method:** `addBon({ cardCode: string, bonNr: string })`
|
||||
|
||||
**Returns:** `Promise<boolean>` - Success status
|
||||
|
||||
## User Workflow
|
||||
|
||||
1. **Enter Bon Number:** User types the receipt number into the input field
|
||||
2. **Validate:** Click "Bon suchen" or press Enter to validate
|
||||
3. **View Details:** If valid, date and total amount are displayed
|
||||
4. **Redeem:** Click "Für Kunden verbuchen" to process redemption
|
||||
5. **Confirmation:** Success dialog appears with auto-close
|
||||
6. **Reset:** Form automatically clears for next redemption
|
||||
|
||||
## Error Handling
|
||||
|
||||
The component handles multiple error scenarios:
|
||||
|
||||
- Missing card code validation
|
||||
- Missing or invalid Bon number
|
||||
- Backend validation failures
|
||||
- Redemption API errors
|
||||
- Network errors
|
||||
|
||||
All errors are displayed inline with user-friendly German messages and logged via `@isa/core/logging`.
|
||||
|
||||
## Accessibility Features
|
||||
|
||||
- ARIA labels on all interactive elements
|
||||
- ARIA live regions for status messages
|
||||
- ARIA invalid and describedby for error states
|
||||
- Semantic HTML with proper roles
|
||||
- Keyboard navigation support (Enter key for validation)
|
||||
|
||||
## E2E Testing Attributes
|
||||
|
||||
All components include `data-what` and `data-which` attributes for automated testing:
|
||||
|
||||
- `bon-redemption-container`
|
||||
- `bon-number-input`
|
||||
- `validate-bon-button`
|
||||
- `bon-success-message`
|
||||
- `bon-error-message`
|
||||
- `bon-details`
|
||||
- `bon-date`
|
||||
- `bon-total`
|
||||
- `redeem-bon-button`
|
||||
|
||||
## Dependencies
|
||||
|
||||
### Internal Dependencies
|
||||
|
||||
- `@isa/crm/data-access` - BonRedemptionStore, CustomerBonCheckResource, CustomerBonRedemptionFacade
|
||||
- `@isa/ui/dialog` - injectFeedbackDialog, injectFeedbackErrorDialog
|
||||
- `@isa/ui/buttons` - ButtonComponent, TextButtonComponent
|
||||
- `@isa/ui/input-controls` - TextFieldComponent, InputControlDirective, TextFieldClearComponent
|
||||
- `@isa/core/logging` - logger factory for structured logging
|
||||
- `@isa/common/data-access` - ResponseArgsError
|
||||
|
||||
### External Dependencies
|
||||
|
||||
- `@angular/core` - Angular framework
|
||||
- `@angular/common` - DecimalPipe for number formatting
|
||||
- `@angular/forms` - FormsModule for ngModel
|
||||
|
||||
## Styling
|
||||
|
||||
The component uses Tailwind CSS with ISA design system tokens:
|
||||
|
||||
- Background: `bg-isa-neutral-200`
|
||||
- Text: `isa-text-body-1-bold`, `isa-text-body-2-regular`, etc.
|
||||
- Colors: `text-isa-accent-green`, `text-isa-accent-red`, `text-isa-neutral-600`
|
||||
- Spacing: Standard Tailwind spacing utilities
|
||||
|
||||
## Configuration
|
||||
|
||||
**Project:** `crm-feature-customer-bon-redemption`
|
||||
|
||||
**Prefix:** `crm`
|
||||
|
||||
**Tags:** `skip:ci`
|
||||
|
||||
**Testing:** Vitest configuration with `@nx/vite:test` executor
|
||||
|
||||
**Linting:** ESLint configuration with `@nx/eslint:lint` executor
|
||||
|
||||
@@ -1,7 +1,202 @@
|
||||
# crm-feature-customer-booking
|
||||
# @isa/crm/feature/customer-booking
|
||||
|
||||
This library was generated with [Nx](https://nx.dev).
|
||||
A standalone Angular component that enables customer loyalty card point booking and reversal with configurable booking reasons.
|
||||
|
||||
## Running unit tests
|
||||
## Overview
|
||||
|
||||
Run `nx test crm-feature-customer-booking` to execute the unit tests.
|
||||
This feature library provides a self-contained booking interface for managing customer loyalty card points. It allows staff members to add or subtract points from customer cards based on configurable booking reasons (e.g., purchase bonuses, corrections, refunds). The component handles point calculation, validation, and provides real-time feedback through dialog notifications.
|
||||
|
||||
The booking operation automatically associates the transaction with the current booking partner store and supports both positive (credit) and negative (debit) point adjustments with multiplier factors.
|
||||
|
||||
## Architecture
|
||||
|
||||
- **Type**: Feature library (smart component with data access integration)
|
||||
- **Prefix**: `crm`
|
||||
- **Tags**: `skip:ci`
|
||||
- **Testing**: Vitest
|
||||
|
||||
## Components
|
||||
|
||||
### CrmFeatureCustomerBookingComponent
|
||||
|
||||
A standalone component that provides a complete booking interface for customer loyalty card points.
|
||||
|
||||
**Selector**: `crm-customer-booking`
|
||||
|
||||
**Inputs**:
|
||||
- `cardCode: string | undefined` - The customer loyalty card code to book points against
|
||||
|
||||
**Outputs**:
|
||||
- `booked: void` - Emitted when a booking operation completes successfully
|
||||
|
||||
**Features**:
|
||||
- **Dynamic booking reasons**: Loads available booking reasons from backend with automatic caching
|
||||
- **Point calculation**: Supports multiplier factors per booking reason (e.g., 1€ = 10 points)
|
||||
- **Real-time validation**: Disables booking when inputs are invalid or incomplete
|
||||
- **Loading states**: Shows pending states during API operations
|
||||
- **Error handling**: Displays user-friendly error messages via dialog
|
||||
- **Auto-reset**: Clears form inputs after successful booking
|
||||
|
||||
**State Management**:
|
||||
- `points: Signal<number | undefined>` - User-entered point value
|
||||
- `selectedReasonKey: Signal<string | undefined>` - Currently selected booking reason
|
||||
- `isBooking: Signal<boolean>` - Tracks ongoing booking operation
|
||||
- `bookingReasons: Signal<BookingReason[]>` - Available booking reason options
|
||||
- `calculatedPoints: Computed<number>` - Final point value after applying multiplier
|
||||
- `disableBooking: Computed<boolean>` - Whether booking button should be disabled
|
||||
|
||||
**Template Features**:
|
||||
- Positive integer input directive for point entry
|
||||
- Dropdown selector for booking reasons with visual feedback (+/-)
|
||||
- Tooltip with point conversion explanation (1€ = 10 points)
|
||||
- Primary action button with loading state
|
||||
- E2E test attributes (`data-what`, `data-which`) on all interactive elements
|
||||
|
||||
## Usage
|
||||
|
||||
### Basic Integration
|
||||
|
||||
```typescript
|
||||
import { CrmFeatureCustomerBookingComponent } from '@isa/crm/feature/customer-booking';
|
||||
|
||||
@Component({
|
||||
selector: 'app-customer-detail',
|
||||
imports: [CrmFeatureCustomerBookingComponent],
|
||||
template: `
|
||||
<crm-customer-booking
|
||||
[cardCode]="customerCardCode()"
|
||||
(booked)="onPointsBooked()"
|
||||
/>
|
||||
`,
|
||||
})
|
||||
export class CustomerDetailComponent {
|
||||
customerCardCode = signal<string>('CARD123456');
|
||||
|
||||
onPointsBooked() {
|
||||
console.log('Points successfully booked');
|
||||
// Refresh customer data, close dialog, etc.
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Integration Notes
|
||||
|
||||
1. **Card code requirement**: The component only renders when a valid `cardCode` is provided
|
||||
2. **Booking partner context**: Automatically fetches and uses the current booking partner store
|
||||
3. **Dialog dependencies**: Requires `@isa/ui/dialog` feedback dialogs to be configured in the app
|
||||
4. **Resource management**: `CustomerBookingReasonsResource` is provided at component level for automatic cleanup
|
||||
|
||||
## Dependencies
|
||||
|
||||
### Internal Libraries
|
||||
|
||||
- **`@isa/crm/data-access`** - Core data access layer
|
||||
- `CustomerBookingReasonsResource` - Loads available booking reasons
|
||||
- `CustomerCardBookingFacade` - Handles booking API operations
|
||||
- **`@isa/ui/buttons`** - Button component
|
||||
- **`@isa/ui/input-controls`** - Dropdown components
|
||||
- **`@isa/ui/dialog`** - Feedback dialog utilities
|
||||
- **`@isa/ui/tooltip`** - Tooltip component
|
||||
- **`@isa/utils/positive-integer-input`** - Input validation directive
|
||||
- **`@isa/core/logging`** - Logging infrastructure
|
||||
|
||||
### Angular Dependencies
|
||||
|
||||
- `@angular/core` - Component framework
|
||||
- `@angular/forms` - FormsModule for ngModel bindings
|
||||
|
||||
## API Integration
|
||||
|
||||
### Booking Request
|
||||
|
||||
```typescript
|
||||
interface BookingRequest {
|
||||
cardCode: string;
|
||||
booking: {
|
||||
points: number; // Calculated points (input × multiplier)
|
||||
reason: string; // Selected booking reason key
|
||||
storeId: string | undefined; // Current booking partner store
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### Booking Reasons
|
||||
|
||||
```typescript
|
||||
interface BookingReason {
|
||||
key: string; // Unique identifier
|
||||
label: string; // Display label (e.g., "Gutschrift", "Storno")
|
||||
value: number; // Multiplier factor (positive or negative)
|
||||
}
|
||||
```
|
||||
|
||||
## Styling
|
||||
|
||||
The component uses Tailwind CSS utility classes with the ISA design system:
|
||||
|
||||
- **Container**: `h-[15.5rem]` fixed height, rounded card with neutral background
|
||||
- **Layout**: Flexbox column with 4-unit gap spacing
|
||||
- **Typography**: ISA text classes (`isa-text-body-1-bold`, `isa-text-body-2-regular`)
|
||||
- **Colors**: ISA neutral palette (`isa-neutral-200`, `isa-neutral-900`)
|
||||
- **Input styling**: Custom number input with hidden spin buttons for clean appearance
|
||||
|
||||
## Error Handling
|
||||
|
||||
The component handles several error scenarios:
|
||||
|
||||
1. **Missing card code**: "Kein Karten-Code vorhanden"
|
||||
2. **No booking reason selected**: "Kein Buchungsgrund ausgewählt"
|
||||
3. **Zero points**: "Punktezahl muss größer als 0 sein"
|
||||
4. **API failures**: Generic error message with fallback to error object message
|
||||
|
||||
All errors are logged via `@isa/core/logging` with context and displayed to users via error feedback dialog.
|
||||
|
||||
## Accessibility
|
||||
|
||||
- **ARIA attributes**: E2E test attributes on all interactive elements
|
||||
- **Semantic HTML**: Proper input types and labels
|
||||
- **Keyboard navigation**: Full keyboard support via native controls
|
||||
- **Focus management**: Standard Angular focus behavior
|
||||
|
||||
## Testing
|
||||
|
||||
### Test Setup
|
||||
|
||||
The library uses Vitest for testing. Test files follow the pattern:
|
||||
- Component tests: `*.component.spec.ts`
|
||||
- Test setup: `test-setup.ts`
|
||||
|
||||
### Test Coverage Areas
|
||||
|
||||
- Component rendering with/without card code
|
||||
- Point calculation with different multipliers
|
||||
- Booking reason selection and validation
|
||||
- Successful booking flow with dialog feedback
|
||||
- Error scenarios and error dialog display
|
||||
- Input validation and form state management
|
||||
|
||||
## Development
|
||||
|
||||
### Build
|
||||
|
||||
```bash
|
||||
nx build crm-feature-customer-booking
|
||||
```
|
||||
|
||||
### Test
|
||||
|
||||
```bash
|
||||
nx test crm-feature-customer-booking
|
||||
```
|
||||
|
||||
### Lint
|
||||
|
||||
```bash
|
||||
nx lint crm-feature-customer-booking
|
||||
```
|
||||
|
||||
## Related Libraries
|
||||
|
||||
- **`@isa/crm/feature/customer-card`** - Customer card overview and management
|
||||
- **`@isa/crm/data-access`** - Shared CRM data access services
|
||||
- **`@isa/checkout/feature/reward`** - Checkout reward redemption feature
|
||||
|
||||
@@ -1,7 +1,145 @@
|
||||
# crm-feature-customer-card-transactions
|
||||
# @isa/crm/feature/customer-card-transactions
|
||||
|
||||
This library was generated with [Nx](https://nx.dev).
|
||||
A standalone Angular component that displays customer loyalty card transaction history with automatic data loading and refresh capabilities.
|
||||
|
||||
## Running unit tests
|
||||
## Overview
|
||||
|
||||
Run `nx test crm-feature-customer-card-transactions` to execute the unit tests.
|
||||
This feature library provides a comprehensive transaction history view for customer loyalty cards in the CRM system. It displays the last 50 transactions in a tabular format, showing transaction details such as date, booking type, amount, receipt reference, and loyalty points earned or burned. The component uses Angular's Resource API for reactive data loading and handles loading, error, and empty states gracefully.
|
||||
|
||||
The component is designed to be embedded within a parent component (such as a customer detail view) and communicates back through output events for coordinated data refreshes.
|
||||
|
||||
## Components
|
||||
|
||||
### CrmFeatureCustomerCardTransactionsComponent
|
||||
|
||||
**Selector:** `crm-customer-card-transactions`
|
||||
|
||||
**Purpose:** Displays a paginated table of customer loyalty card transactions with automatic loading and refresh functionality.
|
||||
|
||||
#### Inputs
|
||||
|
||||
- `customerId: Signal<number | undefined>` - The customer ID whose transactions should be loaded. When this value changes, the component automatically fetches new transaction data.
|
||||
|
||||
#### Outputs
|
||||
|
||||
- `reload: EventEmitter<void>` - Emitted when the user clicks the refresh button. The parent component is responsible for coordinating multi-resource updates across the page.
|
||||
|
||||
#### Features
|
||||
|
||||
- **Reactive Data Loading**: Uses `CustomerCardTransactionsResource` from `@isa/crm/data-access` for automatic data fetching with the Resource API pattern
|
||||
- **Transaction Display**: Shows up to 50 most recent transactions in a styled CDK table
|
||||
- **Transaction Types**: Visually differentiates between EARN (points gained, green with up arrow) and BURN (points spent, red with down arrow) transactions
|
||||
- **Loading States**: Displays loading indicator while fetching data
|
||||
- **Error Handling**: Shows user-friendly error messages when data loading fails
|
||||
- **Empty State**: Displays an empty state component when no transactions exist
|
||||
- **Manual Refresh**: Icon button to trigger data reload via parent component
|
||||
- **Performance Optimized**: Uses `trackBy` function for efficient list rendering and `OnPush` change detection strategy
|
||||
|
||||
#### Table Columns
|
||||
|
||||
1. **Datum (Date)**: Transaction timestamp formatted as `dd.MM.yyyy HH:mm:ss`
|
||||
2. **Buchungsart (Booking Type)**: Transaction reason/description
|
||||
3. **Umsatz (Amount)**: Transaction amount in EUR with proper decimal formatting
|
||||
4. **Bon-Nummer (Reference)**: Receipt/transaction reference number
|
||||
5. **Lesepunkte (Points)**: Loyalty points with visual indicators (green up arrow for earned, red down arrow for burned)
|
||||
|
||||
#### Change Detection
|
||||
|
||||
The component uses `ChangeDetectionStrategy.OnPush` for optimal performance and relies on signals for reactive state management.
|
||||
|
||||
## Usage
|
||||
|
||||
### Basic Integration
|
||||
|
||||
```typescript
|
||||
import { CrmFeatureCustomerCardTransactionsComponent } from '@isa/crm/feature/customer-card-transactions';
|
||||
|
||||
@Component({
|
||||
selector: 'app-customer-detail',
|
||||
imports: [CrmFeatureCustomerCardTransactionsComponent],
|
||||
template: `
|
||||
<crm-customer-card-transactions
|
||||
[customerId]="selectedCustomerId()"
|
||||
(reload)="reloadAllCustomerData()"
|
||||
/>
|
||||
`
|
||||
})
|
||||
export class CustomerDetailComponent {
|
||||
selectedCustomerId = signal<number | undefined>(undefined);
|
||||
|
||||
reloadAllCustomerData() {
|
||||
// Coordinate refresh of all customer-related resources
|
||||
this.customerResource.reload();
|
||||
this.transactionsResource.reload();
|
||||
// ... other resources
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Prerequisites
|
||||
|
||||
This component requires the `CustomerCardTransactionsResource` to be provided in the dependency injection tree. Ensure that the resource is properly configured in your parent component or route providers.
|
||||
|
||||
```typescript
|
||||
import { CustomerCardTransactionsResource } from '@isa/crm/data-access';
|
||||
|
||||
@Component({
|
||||
providers: [CustomerCardTransactionsResource],
|
||||
// ...
|
||||
})
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
### Internal Dependencies (@isa/*)
|
||||
|
||||
- **@isa/crm/data-access** - Provides `CustomerCardTransactionsResource` for transaction data fetching
|
||||
- **@isa/ui/empty-state** - Empty state component for when no transactions exist
|
||||
- **@isa/ui/buttons** - Icon button component for refresh functionality
|
||||
- **@isa/icons** - Icon library providing `isaActionPolygonUp`, `isaActionPolygonDown`, and `isaActionRefresh` icons
|
||||
- **@isa/core/logging** - Logging utilities for component diagnostics
|
||||
|
||||
### External Dependencies
|
||||
|
||||
- **@angular/cdk/table** - CDK table module for performant table rendering
|
||||
- **@angular/common** - DatePipe and DecimalPipe for data formatting
|
||||
- **@ng-icons/core** - Icon component infrastructure
|
||||
- **@generated/swagger/crm-api** - Generated API types (`LoyaltyBookingInfoDTO`)
|
||||
|
||||
## Architecture Notes
|
||||
|
||||
- **Standalone Component**: Fully standalone with no NgModule dependencies
|
||||
- **Signal-based**: Uses Angular signals for reactive state management
|
||||
- **Resource API Pattern**: Leverages Angular's Resource API through the data-access layer
|
||||
- **Parent-coordinated Refresh**: Delegates refresh coordination to parent component to maintain consistency across multiple related data sources
|
||||
- **Design System Compliant**: Uses Tailwind CSS classes following the ISA design system
|
||||
- **Accessibility**: Implements semantic HTML table structure with proper header associations
|
||||
|
||||
## Styling
|
||||
|
||||
The component includes custom CSS for the CDK table with the following characteristics:
|
||||
|
||||
- Rounded header row with gray background (`#ced4da`)
|
||||
- Row spacing for visual separation
|
||||
- Left-aligned text columns with right-aligned numeric columns
|
||||
- Responsive padding and consistent typography
|
||||
- Transparent icon button styling for the refresh action
|
||||
|
||||
## Data Model
|
||||
|
||||
The component consumes `LoyaltyBookingInfoDTO` from the CRM API, which includes:
|
||||
|
||||
- `date`: Transaction timestamp
|
||||
- `reason`: Transaction description/reason
|
||||
- `amount`: Transaction amount in EUR
|
||||
- `reference`: Receipt/transaction reference number
|
||||
- `points`: Loyalty points affected
|
||||
- `type`: Transaction type (`EARN` or `BURN`)
|
||||
|
||||
## Testing
|
||||
|
||||
Run unit tests with:
|
||||
|
||||
```bash
|
||||
nx test crm-feature-customer-card-transactions
|
||||
```
|
||||
|
||||
@@ -1,7 +1,276 @@
|
||||
# crm-feature-customer-loyalty-cards
|
||||
# @isa/crm/feature/customer-loyalty-cards
|
||||
|
||||
This library was generated with [Nx](https://nx.dev).
|
||||
Customer loyalty cards feature module for displaying and managing customer bonus cards (Kundenkarten) with points tracking and card management actions.
|
||||
|
||||
## Running unit tests
|
||||
## Overview
|
||||
|
||||
Run `nx test crm-feature-customer-loyalty-cards` to execute the unit tests.
|
||||
This feature library provides a complete customer loyalty cards management interface for the CRM module. It displays all bonus cards associated with a customer, including:
|
||||
|
||||
- Points summary with total Lesepunkte (reading points) from the primary card
|
||||
- Call-to-action button to navigate to the Prämienshop (rewards shop)
|
||||
- Interactive carousel for browsing multiple customer cards with barcodes
|
||||
- Card management actions: add new cards, lock/unlock cards
|
||||
- Visual indication of blocked cards with overlay states
|
||||
- Automatic sorting of blocked cards to the end of the carousel
|
||||
|
||||
The main component uses the Resource API pattern via `CustomerBonusCardsResource` for reactive data loading with automatic race condition prevention.
|
||||
|
||||
## Architecture
|
||||
|
||||
This is a **feature library** (prefix: `crm`) containing:
|
||||
- Main container component that orchestrates the feature
|
||||
- Presentational components for cards, points summary, and carousel
|
||||
- Integration with `@isa/crm/data-access` for state management
|
||||
- Reactive data loading using Angular's Resource API
|
||||
|
||||
## Components
|
||||
|
||||
### CustomerLoyaltyCardsComponent
|
||||
|
||||
Main container component for the customer loyalty cards feature.
|
||||
|
||||
**Selector:** `crm-customer-loyalty-cards`
|
||||
|
||||
**Inputs:**
|
||||
- `customerId: number` (required) - Customer ID to load loyalty cards for. When changed, triggers automatic reload of bonus cards.
|
||||
- `tabId: number` (required) - Tab ID for telemetry/logging purposes.
|
||||
|
||||
**Outputs:**
|
||||
- `navigateToPraemienshop: void` - Emitted when user clicks the "Zum Prämienshop" button. Parent must handle navigation using the autoTriggerContinueFn pattern.
|
||||
- `cardUpdated: void` - Emitted when a card has been added, locked, or unlocked. Parent should reload cards and transactions.
|
||||
|
||||
**State:**
|
||||
- `cards: Signal<BonusCardInfo[] | undefined>` - All bonus cards for the selected customer
|
||||
- `isLoading: Signal<boolean>` - Loading state for bonus cards
|
||||
- `error: Signal<Error | undefined>` - Error state from loading bonus cards
|
||||
|
||||
**Example:**
|
||||
```html
|
||||
<crm-customer-loyalty-cards
|
||||
[customerId]="selectedCustomerId()"
|
||||
[tabId]="currentTabId()"
|
||||
(navigateToPraemienshop)="handlePraemienshopNavigation()"
|
||||
(cardUpdated)="reloadCardsAndTransactions()"
|
||||
/>
|
||||
```
|
||||
|
||||
### CustomerCardPointsSummaryComponent
|
||||
|
||||
Displays the points summary with CTA to Prämienshop.
|
||||
|
||||
**Selector:** `crm-customer-card-points-summary`
|
||||
|
||||
**Inputs:**
|
||||
- `cards: BonusCardInfo[]` (required) - All bonus cards for the customer
|
||||
|
||||
**Outputs:**
|
||||
- `navigateToPraemienshop: void` - Emitted when user clicks "Zum Prämienshop" button
|
||||
|
||||
**Features:**
|
||||
- Extracts total points from primary card
|
||||
- Formats points with German thousands separator (dot)
|
||||
- Centered layout matching Figma design
|
||||
|
||||
### CustomerCardsCarouselComponent
|
||||
|
||||
Carousel container for displaying multiple customer loyalty cards with horizontal scrolling.
|
||||
|
||||
**Selector:** `crm-customer-cards-carousel`
|
||||
|
||||
**Inputs:**
|
||||
- `cards: BonusCardInfo[]` (required) - All bonus cards to display in carousel
|
||||
|
||||
**Outputs:**
|
||||
- `cardLocked: void` - Emitted when a card has been successfully locked or unlocked
|
||||
|
||||
**Features:**
|
||||
- Horizontal scrolling with navigation arrows (via `@isa/ui/carousel`)
|
||||
- Automatic sorting: blocked cards always appear at the end
|
||||
- Gap spacing between cards
|
||||
- Smooth scrolling animations
|
||||
|
||||
### CustomerCardComponent
|
||||
|
||||
Individual customer loyalty card display component.
|
||||
|
||||
**Selector:** `crm-customer-card`
|
||||
|
||||
**Inputs:**
|
||||
- `card: BonusCardInfo` (required) - Bonus card data to display
|
||||
|
||||
**Outputs:**
|
||||
- `cardLocked: void` - Emitted when a card has been successfully locked or unlocked
|
||||
|
||||
**Features:**
|
||||
- Displays card type (Kundenkarte/Mitarbeitendenkarte)
|
||||
- Shows card number and customer name
|
||||
- Renders barcode using `@isa/shared/barcode`
|
||||
- Blocked state overlay for inactive cards
|
||||
- Fixed dimensions: 337×213px (based on Figma design)
|
||||
|
||||
### AddCustomerCardComponent
|
||||
|
||||
Button and dialog for adding new customer cards.
|
||||
|
||||
**Selector:** `crm-add-customer-card`
|
||||
|
||||
**Outputs:**
|
||||
- `cardAdded: void` - Emitted when a card has been successfully added
|
||||
|
||||
**Features:**
|
||||
- Opens text input dialog for scanning or entering 8-digit card code
|
||||
- Validates card code input (required)
|
||||
- Calls `CustomerCardsFacade.addCard()` to add card
|
||||
- Shows success feedback dialog
|
||||
- Emits event for parent to reload cards
|
||||
|
||||
### LockCustomerCardComponent
|
||||
|
||||
Component for locking/unlocking customer cards.
|
||||
|
||||
**Selector:** `crm-lock-customer-card`
|
||||
|
||||
**Inputs:**
|
||||
- `isActive: boolean` (required) - Whether the card is currently active or blocked
|
||||
- `cardCode: string` (required) - Card code to lock/unlock
|
||||
|
||||
**Outputs:**
|
||||
- `cardLocked: void` - Emitted when a card has been successfully locked or unlocked
|
||||
|
||||
**Features:**
|
||||
- Shows "Karte sperren" button for active cards
|
||||
- Shows "Karte entsperren" button for blocked cards
|
||||
- Handles loading state during API calls
|
||||
- Shows success/error feedback dialogs
|
||||
- Emits event for parent to reload cards
|
||||
|
||||
## Usage
|
||||
|
||||
### Integration in Parent Component
|
||||
|
||||
```typescript
|
||||
import { Component, signal } from '@angular/core';
|
||||
import { CustomerLoyaltyCardsComponent } from '@isa/crm/feature/customer-loyalty-cards';
|
||||
|
||||
@Component({
|
||||
selector: 'app-customer-detail',
|
||||
imports: [CustomerLoyaltyCardsComponent],
|
||||
template: `
|
||||
<crm-customer-loyalty-cards
|
||||
[customerId]="customerId()"
|
||||
[tabId]="tabId()"
|
||||
(navigateToPraemienshop)="navigateToPraemienshop()"
|
||||
(cardUpdated)="handleCardUpdated()"
|
||||
/>
|
||||
`
|
||||
})
|
||||
export class CustomerDetailComponent {
|
||||
customerId = signal(12345);
|
||||
tabId = signal(1);
|
||||
|
||||
navigateToPraemienshop(): void {
|
||||
// Use autoTriggerContinueFn pattern to navigate with proper customer selection
|
||||
this.router.navigate(['/praemienshop'], {
|
||||
queryParams: { customerId: this.customerId() }
|
||||
});
|
||||
}
|
||||
|
||||
handleCardUpdated(): void {
|
||||
// Reload cards and related data (e.g., transactions)
|
||||
this.customerService.reload(this.customerId());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Required Provider Setup
|
||||
|
||||
The component requires `CustomerBonusCardsResource` to be provided:
|
||||
|
||||
```typescript
|
||||
import { CustomerBonusCardsResource } from '@isa/crm/data-access';
|
||||
|
||||
@Component({
|
||||
providers: [CustomerBonusCardsResource],
|
||||
// ...
|
||||
})
|
||||
```
|
||||
|
||||
This is typically provided at the feature route or parent component level to enable scoped resource management.
|
||||
|
||||
## Dependencies
|
||||
|
||||
### Internal Dependencies
|
||||
|
||||
**Data Access:**
|
||||
- `@isa/crm/data-access` - State management and API integration
|
||||
- `CustomerBonusCardsResource` - Resource API for loading bonus cards
|
||||
- `CustomerCardsFacade` - Facade for add/lock/unlock operations
|
||||
- `BonusCardInfo` - Type definition for bonus card data
|
||||
|
||||
**UI Components:**
|
||||
- `@isa/ui/buttons` - `ButtonComponent`, `TextButtonComponent`
|
||||
- `@isa/ui/carousel` - `CarouselComponent` for card scrolling
|
||||
- `@isa/ui/dialog` - Dialog services for add card and feedback
|
||||
- `@isa/shared/barcode` - `BarcodeComponent` for rendering card barcodes
|
||||
|
||||
**Core:**
|
||||
- `@isa/core/logging` - Logger factory for consistent logging
|
||||
|
||||
### External Dependencies
|
||||
|
||||
- `@angular/core` - Angular framework
|
||||
- `@angular/forms` - Form validation for card code input
|
||||
- `@angular/cdk/coercion` - Number input coercion
|
||||
- `rxjs` - For dialog result handling
|
||||
|
||||
## State Management
|
||||
|
||||
This feature uses the **Resource API pattern** for state management:
|
||||
|
||||
```typescript
|
||||
// Reactive loading with automatic race condition prevention
|
||||
effect(() => {
|
||||
const customerId = this.customerId();
|
||||
this.#bonusCardsResource.params({ customerId });
|
||||
});
|
||||
|
||||
// Signals for state
|
||||
readonly cards = this.#bonusCardsResource.resource.value;
|
||||
readonly isLoading = this.#bonusCardsResource.resource.isLoading;
|
||||
readonly error = this.#bonusCardsResource.resource.error;
|
||||
```
|
||||
|
||||
This approach provides:
|
||||
- Automatic race condition handling (cancels previous requests)
|
||||
- Declarative resource management without RxJS subscriptions
|
||||
- Reactive updates when `customerId` changes
|
||||
- Built-in loading and error states
|
||||
|
||||
## Testing
|
||||
|
||||
The library uses Vitest for testing with Angular Testing Utilities. Test files follow the pattern:
|
||||
|
||||
- `*.component.spec.ts` - Component unit tests
|
||||
- Uses `ComponentFixture` and `TestBed` for component testing
|
||||
- Mocks for `CustomerBonusCardsResource` and `CustomerCardsFacade`
|
||||
|
||||
Run tests:
|
||||
```bash
|
||||
nx test crm-feature-customer-loyalty-cards
|
||||
```
|
||||
|
||||
## Design
|
||||
|
||||
Based on Figma designs with:
|
||||
- Card dimensions: 337×213px
|
||||
- Black header, white footer
|
||||
- Barcode sizing: 4.5rem height, 0.125rem width, 0.5rem margin
|
||||
- Carousel with horizontal scrolling and navigation arrows
|
||||
- Blocked cards display overlay and sort to end of carousel
|
||||
|
||||
## Notes
|
||||
|
||||
- **Blocked cards sorting:** Per Figma annotation, blocked cards are always sorted to the end of the carousel ("gesperrte Karte immer nach hinten")
|
||||
- **Primary card points:** Only the primary card's points are displayed in the summary
|
||||
- **Navigation pattern:** Uses output events instead of direct router navigation to support the autoTriggerContinueFn pattern for proper customer context
|
||||
- **Card updates:** All card mutation operations (add, lock, unlock) emit events for parent coordination to reload related data
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
# icons
|
||||
# @isa/icons
|
||||
|
||||
A centralized icon library for the ISA-Frontend monorepo providing inline SVG icons as string constants.
|
||||
|
||||
## Overview
|
||||
|
||||
The `@isa/icons` library is a centralized icon repository for the ISA Frontend monorepo. It provides **75 inline SVG icons** as string constants, organized by functional categories. All icons are exported as ready-to-use SVG markup strings that can be embedded directly in Angular templates.
|
||||
The `@isa/icons` library is a centralized icon repository for the ISA Frontend monorepo. It provides **78 inline SVG icons** as string constants, organized by functional categories. All icons are exported as ready-to-use SVG markup strings designed to work seamlessly with the `@ng-icons/core` library in Angular components.
|
||||
|
||||
Each icon is:
|
||||
- Exported as a TypeScript constant with SVG string content
|
||||
- Named with a semantic prefix indicating its category
|
||||
- Ready for use with `@ng-icons/core`'s `provideIcons()` function
|
||||
- Styled with `fill="currentColor"` to inherit text color from parent elements
|
||||
|
||||
## Icon Categories
|
||||
|
||||
@@ -11,7 +19,7 @@ The library contains **9 distinct categories** of icons:
|
||||
| Category | Count | Purpose | Examples |
|
||||
|----------|-------|---------|----------|
|
||||
| **Navigation** | 23 | Main navigation, UI controls, menu items | `isaNavigationSidemenu`, `isaNavigationCart`, `isaNavigationKunden` |
|
||||
| **Action** | 15 | User actions, buttons, interactive elements | `isaActionChevron`, `isaActionPlus`, `isaActionSearch`, `isaActionEdit` |
|
||||
| **Action** | 18 | User actions, buttons, interactive elements | `isaActionChevron`, `isaActionPlus`, `isaActionSearch`, `isaActionEdit` |
|
||||
| **Reviews** | 13 | Small-sized icons for reviews/ratings UI | `isaReviewsStarSmall`, `isaReviewsChevronSmall`, `isaReviewsCheckSmall` |
|
||||
| **Artikel** | 8 | Product format indicators | `isaArtikelTaschenbuch`, `isaArtikelCd`, `isaArtikelEbook`, `isaArtikelGame` |
|
||||
| **Delivery** | 7 | Delivery/fulfillment methods | `isaDeliveryWarenausgabe`, `isaDeliveryVersand`, `isaDeliveryDownload` |
|
||||
@@ -27,45 +35,77 @@ The library contains **9 distinct categories** of icons:
|
||||
- **Style:** All icons use `fill="currentColor"` or `stroke="currentColor"` for easy color customization via CSS
|
||||
- **Naming Convention:** `isa[Category][Name]` (e.g., `isaActionChevron`, `isaNavigationCart`)
|
||||
|
||||
## Installation
|
||||
|
||||
Import icons directly from the library:
|
||||
|
||||
```typescript
|
||||
import { isaActionClose, isaNavigationDashboard, ProductFormatIconGroup } from '@isa/icons';
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Import Individual Icons
|
||||
### Using Individual Icons with @ng-icons/core
|
||||
|
||||
Import and register icons with `@ng-icons/core` (recommended):
|
||||
|
||||
```typescript
|
||||
import { isaActionSearch, isaNavigationCart } from '@isa/icons';
|
||||
import { Component } from '@angular/core';
|
||||
import { NgIcon, provideIcons } from '@ng-icons/core';
|
||||
import { isaActionClose, isaActionSearch } from '@isa/icons';
|
||||
|
||||
@Component({
|
||||
selector: 'app-my-component',
|
||||
selector: 'app-search-dialog',
|
||||
standalone: true,
|
||||
imports: [NgIcon],
|
||||
providers: [
|
||||
provideIcons({
|
||||
isaActionClose,
|
||||
isaActionSearch
|
||||
})
|
||||
],
|
||||
template: `
|
||||
<div [innerHTML]="searchIcon"></div>
|
||||
<div [innerHTML]="cartIcon"></div>
|
||||
`,
|
||||
standalone: true,
|
||||
<button>
|
||||
<ng-icon name="isaActionSearch" />
|
||||
Search
|
||||
</button>
|
||||
<button>
|
||||
<ng-icon name="isaActionClose" />
|
||||
Close
|
||||
</button>
|
||||
`
|
||||
})
|
||||
export class MyComponent {
|
||||
searchIcon = isaActionSearch;
|
||||
cartIcon = isaNavigationCart;
|
||||
}
|
||||
export class SearchDialogComponent {}
|
||||
```
|
||||
|
||||
### Import Icon Group
|
||||
### Using Icon Groups
|
||||
|
||||
The library exports `ProductFormatIconGroup`, a pre-configured mapping of product format codes to their corresponding icons:
|
||||
|
||||
```typescript
|
||||
import { Component, input } from '@angular/core';
|
||||
import { NgIcon, provideIcons } from '@ng-icons/core';
|
||||
import { ProductFormatIconGroup } from '@isa/icons';
|
||||
import type { Product } from '@isa/oms/data-access';
|
||||
|
||||
@Component({
|
||||
selector: 'app-product-display',
|
||||
template: `<div [innerHTML]="getProductIcon(product.format)"></div>`,
|
||||
selector: 'app-product-info',
|
||||
standalone: true,
|
||||
imports: [NgIcon],
|
||||
providers: [provideIcons({ ...ProductFormatIconGroup })],
|
||||
template: `
|
||||
<div class="product">
|
||||
<ng-icon [name]="product().format" />
|
||||
<span>{{ product().name }}</span>
|
||||
</div>
|
||||
`
|
||||
})
|
||||
export class ProductDisplayComponent {
|
||||
getProductIcon(format: string): string {
|
||||
return ProductFormatIconGroup[format] || ProductFormatIconGroup['ka'];
|
||||
}
|
||||
export class ProductInfoComponent {
|
||||
product = input.required<Product>();
|
||||
}
|
||||
```
|
||||
|
||||
### Import All Icons
|
||||
### Using All Icons via Namespace
|
||||
|
||||
```typescript
|
||||
import { IsaIcons } from '@isa/icons';
|
||||
@@ -74,16 +114,31 @@ const searchIcon = IsaIcons.isaActionSearch;
|
||||
const cartIcon = IsaIcons.isaNavigationCart;
|
||||
```
|
||||
|
||||
## Color Customization
|
||||
### Direct SVG String Usage
|
||||
|
||||
All icons use `currentColor`, allowing easy theming via CSS color inheritance:
|
||||
Icons can be used directly as SVG strings (use with caution):
|
||||
|
||||
```typescript
|
||||
import { isaActionCheck } from '@isa/icons';
|
||||
|
||||
// Direct HTML insertion (use with [innerHTML] and DomSanitizer)
|
||||
const checkIconSvg = isaActionCheck;
|
||||
```
|
||||
|
||||
## Styling Icons
|
||||
|
||||
Icons use `fill="currentColor"` and inherit the text color from their parent element:
|
||||
|
||||
```html
|
||||
<!-- Icon inherits text color from parent -->
|
||||
<div class="text-isa-accent-500" [innerHTML]="isaActionSearch"></div>
|
||||
<button class="text-primary">
|
||||
<ng-icon name="isaActionCheck" />
|
||||
<!-- Icon will be colored with text-primary color -->
|
||||
</button>
|
||||
|
||||
<!-- Icon inherits from inline style -->
|
||||
<div style="color: #FF5733;" [innerHTML]="isaNavigationCart"></div>
|
||||
<!-- Using Tailwind classes -->
|
||||
<div class="text-isa-accent-500">
|
||||
<ng-icon name="isaActionSearch" />
|
||||
</div>
|
||||
```
|
||||
|
||||
## Icon Groups
|
||||
@@ -110,76 +165,133 @@ const icon = ProductFormatIconGroup['tb']; // Gets paperback icon
|
||||
|
||||
## Available Icons
|
||||
|
||||
### Navigation Icons (23)
|
||||
- `isaNavigationSidemenu`, `isaNavigationCart`, `isaNavigationKunden`, `isaNavigationFilialen`
|
||||
- `isaNavigationRuecklage`, `isaNavigationRemission`, `isaNavigationWareneingang`, `isaNavigationWws`
|
||||
- `isaNavigationOrder`, `isaNavigationFile`, `isaNavigationDownload`, `isaNavigationPrint`
|
||||
- `isaNavigationCalendar`, `isaNavigationScan`, `isaNavigationSettings`, `isaNavigationHelp`
|
||||
- `isaNavigationLogout`, `isaNavigationHome`, `isaNavigationBack`, `isaNavigationForward`
|
||||
- `isaNavigationClose`, `isaNavigationMenu`, `isaNavigationMore`
|
||||
### Action Icons (18)
|
||||
Icons for common user actions and interactions:
|
||||
- `isaActionChevron` / `isaActionChevronDown`
|
||||
- `isaActionChevronUp`, `isaActionChevronRight`, `isaActionChevronLeft`
|
||||
- `isaActionPolygonDown`, `isaActionPolygonUp`
|
||||
- `isaActionMinus`, `isaActionPlus`
|
||||
- `isaActionClose`, `isaActionCheck`
|
||||
- `isaActionFilter`, `isaActionSort`
|
||||
- `isaActionRefresh`, `isaActionSearch`
|
||||
- `isaActionPrinter`, `isaActionScanner`, `isaActionEdit`
|
||||
|
||||
### Action Icons (15)
|
||||
- `isaActionChevron` / `isaActionChevronDown`, `isaActionChevronUp`, `isaActionChevronLeft`, `isaActionChevronRight`
|
||||
- `isaActionPlus`, `isaActionMinus`, `isaActionSearch`, `isaActionEdit`, `isaActionDelete`
|
||||
- `isaActionSave`, `isaActionCancel`, `isaActionRefresh`, `isaActionFilter`, `isaActionExport`
|
||||
### Navigation Icons (23)
|
||||
Icons for navigation elements and menus:
|
||||
- `isaNavigationSidemenu`, `isaNavigationMore`
|
||||
- `isaNavigationFontsizeDecrease`, `isaNavigationFontsizeIncrease`
|
||||
- `isaNavigationFontsizeChange1`, `isaNavigationFontsizeChange2`
|
||||
- `isaNavigationDashboard`, `isaNavigationAbholfach`
|
||||
- `isaNavigationArtikelsuche`, `isaNavigationBell`
|
||||
- `isaNavigationCalender`, `isaNavigationCalenderEmpty`
|
||||
- `isaNavigationCart`, `isaNavigationKunden`
|
||||
- `isaNavigationLogout`, `isaNavigationMessage`
|
||||
- `isaNavigationRemission`, `isaNavigationReturn`
|
||||
- `isaNavigationSortiment`, `isaNavigationWarenausgabe`, `isaNavigationWareneingang`
|
||||
|
||||
### Product/Artikel Icons (8)
|
||||
Icons representing different product formats:
|
||||
- `isaArtikelCd`, `isaArtikelDigital`
|
||||
- `isaArtikelEbook`, `isaArtikelGame`
|
||||
- `isaArtikelKartoniert`, `isaArtikelSonstige`
|
||||
- `isaArtikelTaschenbuch`, `isaArtikelTolino`
|
||||
|
||||
### Delivery Icons (7)
|
||||
Icons for delivery and shipping methods:
|
||||
- `isaDeliveryB`, `isaDeliveryDownload`
|
||||
- `isaDeliveryRuecklage`, `isaDeliveryVersand`, `isaDeliveryWarenausgabe`
|
||||
|
||||
### Reviews Icons (13)
|
||||
- `isaReviewsStarSmall`, `isaReviewsStarHalfSmall`, `isaReviewsStarEmptySmall`
|
||||
- `isaReviewsChevronSmall`, `isaReviewsCheckSmall`, `isaReviewsCloseSmall`
|
||||
- And 7 more small variants...
|
||||
Small-sized icons for reviews and ratings:
|
||||
- `isaReviewsCartSmall`, `isaReviewsCheckSmall`
|
||||
- `isaReviewsChevronDownSmall`, `isaReviewsChevronLeftSmall`
|
||||
- `isaReviewsChevronRightSmall`, `isaReviewsChevronSmall`
|
||||
- `isaReviewsChevronUpSmall`, `isaReviewsCloseSmall`
|
||||
- `isaReviewsMinusSmall`, `isaReviewsPlusSmall`
|
||||
- `isaReviewsSearchSmall`, `isaReviewsStarFilledSmall`, `isaReviewsStarSmall`
|
||||
|
||||
### Product Format Icons (8)
|
||||
- `isaArtikelTaschenbuch`, `isaArtikelKartoniert`, `isaArtikelCd`, `isaArtikelDvd`
|
||||
- `isaArtikelEbook`, `isaArtikelGame`, `isaArtikelSonstige`, `isaArtikelVinyl`
|
||||
### Other Icons (3)
|
||||
Miscellaneous icons:
|
||||
- `isaOtherGift`, `isaOtherHeart`, `isaOtherInfo`
|
||||
|
||||
### Delivery Method Icons (7)
|
||||
- `isaDeliveryWarenausgabe`, `isaDeliveryVersand`, `isaDeliveryDownload`
|
||||
- `isaDeliveryRuecklage1`, `isaDeliveryRuecklage2`, `isaDeliveryAbholfach`
|
||||
- `isaDeliveryExpress`
|
||||
### Store/Location Icons (2)
|
||||
- `isaFiliale`, `isaFilialeLocation`
|
||||
|
||||
## Size Variants
|
||||
### Loading Icons (2)
|
||||
- `isaLoading`, `isaLoadingSmall`
|
||||
|
||||
The library provides size variants for specific use cases:
|
||||
### Sorting Icons (2)
|
||||
- `isaSortByDownMedium`, `isaSortByUpMedium`
|
||||
|
||||
- **Standard:** 24×24px (most icons)
|
||||
- **Small:** 12×12px (icons with `Small` suffix, e.g., `isaReviewsStarSmall`)
|
||||
## Architecture
|
||||
|
||||
## Security Considerations
|
||||
|
||||
When using icons with `[innerHTML]`, Angular's DomSanitizer automatically handles security. However, if you bypass sanitization, ensure icons come from trusted sources.
|
||||
|
||||
## Performance Notes
|
||||
|
||||
- **Bundle Size:** All 75 icons are included in consuming bundles when imported
|
||||
- **Inline SVGs:** Avoid HTTP requests but increase bundle size
|
||||
- **Tree Shaking:** Only import the icons you need to minimize bundle impact
|
||||
|
||||
## E2E Testing
|
||||
|
||||
When using icons in clickable elements, ensure proper `data-what` and `data-which` attributes on parent elements:
|
||||
|
||||
```html
|
||||
<button
|
||||
data-what="button"
|
||||
data-which="search-action"
|
||||
(click)="search()"
|
||||
>
|
||||
<span [innerHTML]="isaActionSearch"></span>
|
||||
Search
|
||||
</button>
|
||||
**Library Structure:**
|
||||
```
|
||||
libs/icons/
|
||||
├── src/
|
||||
│ ├── lib/
|
||||
│ │ ├── icons.ts # All icon constants (78 icons)
|
||||
│ │ └── icon-groups.ts # Pre-configured icon groups
|
||||
│ └── index.ts # Public API exports
|
||||
├── package.json
|
||||
├── project.json
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## Running Tests
|
||||
**Exports:**
|
||||
- `IsaIcons` - Namespace export containing all icons
|
||||
- `ProductFormatIconGroup` - Pre-configured product format icon mapping
|
||||
- Individual icon constants (e.g., `isaActionClose`, `isaNavigationDashboard`)
|
||||
|
||||
Run unit tests with Jest:
|
||||
## Adding New Icons
|
||||
|
||||
To add a new icon to the library:
|
||||
|
||||
1. **Choose a category** based on the icon's purpose:
|
||||
- `isaAction*` - User actions (close, open, edit, etc.)
|
||||
- `isaNavigation*` - Navigation elements
|
||||
- `isaArtikel*` - Product formats
|
||||
- `isaDelivery*` - Delivery methods
|
||||
- `isaReviews*` - Small icons for reviews
|
||||
- `isaOther*` - Miscellaneous icons
|
||||
- `isaFiliale*` - Store/location icons
|
||||
- `isaLoading*` - Loading indicators
|
||||
- `isaSortBy*` - Sorting controls
|
||||
|
||||
2. **Add the SVG constant** to `/libs/icons/src/lib/icons.ts`:
|
||||
```typescript
|
||||
export const isaActionNewIcon =
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor">...</svg>';
|
||||
```
|
||||
|
||||
3. **Ensure SVG attributes**:
|
||||
- Use `fill="currentColor"` for color inheritance
|
||||
- Standard dimensions: `24x24` (or `11x7` for small icons)
|
||||
- Include proper viewBox for scaling
|
||||
|
||||
4. **Export from library** - Icons are automatically exported via `export * from './lib/icons'` in `index.ts`
|
||||
|
||||
5. **Add to icon groups** (if applicable) in `/libs/icons/src/lib/icon-groups.ts`
|
||||
|
||||
6. **Update this README** with the new icon name in the appropriate category
|
||||
|
||||
## Best Practices
|
||||
|
||||
- Use semantic icon names that describe the action or element (e.g., `isaActionClose` not `isaActionX`)
|
||||
- Register only the icons you need in component providers for optimal bundle size
|
||||
- Use icon groups (`ProductFormatIconGroup`) when working with multiple related icons
|
||||
- Let icons inherit color from parent text color using `currentColor`
|
||||
- Prefer `@ng-icons/core` integration over direct SVG string usage for better Angular integration
|
||||
|
||||
## Dependencies
|
||||
|
||||
- **Runtime**: None (pure SVG string constants)
|
||||
- **Usage**: `@ng-icons/core` (peer dependency for Angular integration)
|
||||
|
||||
## Testing
|
||||
|
||||
Run tests for this library:
|
||||
|
||||
```bash
|
||||
npx nx test icons
|
||||
nx test icons
|
||||
```
|
||||
|
||||
## Related Libraries
|
||||
|
||||
The icons library integrates with:
|
||||
- **UI Component Libraries:** Used extensively in `@isa/ui/*` components
|
||||
- **Feature Libraries:** Referenced in domain features for contextual icons
|
||||
- **Shared Components:** Used in `@isa/shared/*` for product displays and navigation
|
||||
|
||||
@@ -1,7 +1,271 @@
|
||||
# shared-delivery
|
||||
# @isa/shared/delivery
|
||||
|
||||
This library was generated with [Nx](https://nx.dev).
|
||||
A reusable Angular component library for displaying order destination information with support for multiple delivery types (shipping, pickup, in-store, and digital downloads).
|
||||
|
||||
## Running unit tests
|
||||
## Overview
|
||||
|
||||
Run `nx test shared-delivery` to execute the unit tests.
|
||||
The `@isa/shared/delivery` library provides the `OrderDestinationComponent`, a flexible component that displays order destination details including delivery type icons, recipient information, and addresses. It automatically adapts its display based on the order type (Delivery, Pickup, In-Store, Download) and supports estimated delivery date ranges.
|
||||
|
||||
**Key Features:**
|
||||
- Automatic icon selection based on order type
|
||||
- Support for shipping addresses, branch locations, and digital delivery
|
||||
- Integrated address display with `@isa/shared/address`
|
||||
- Estimated delivery date formatting
|
||||
- Responsive layout with Tailwind CSS
|
||||
- Full accessibility support (ARIA attributes, semantic HTML)
|
||||
- E2E testing attributes (`data-what`)
|
||||
|
||||
## Installation
|
||||
|
||||
```ts
|
||||
import { OrderDestinationComponent } from '@isa/shared/delivery';
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### Components
|
||||
|
||||
#### `OrderDestinationComponent`
|
||||
|
||||
**Selector:** `shared-order-destination`
|
||||
|
||||
Displays order destination information with an appropriate icon and formatted address based on the order type.
|
||||
|
||||
**Inputs:**
|
||||
|
||||
| Input | Type | Required | Default | Description |
|
||||
|-------|------|----------|---------|-------------|
|
||||
| `orderType` | `OrderTypeFeature` | Yes | - | Type of order (Delivery, Pickup, InStore, Download, B2BShipping, DigitalShipping) |
|
||||
| `shippingAddress` | `ShippingAddress` | No | - | Shipping address for delivery orders |
|
||||
| `branch` | `Branch` | No | - | Branch information for pickup/in-store orders |
|
||||
| `estimatedDelivery` | `EstimatedDelivery \| null` | No | - | Estimated delivery date range |
|
||||
| `underline` | `boolean` | No | `false` | Whether to underline the order type label |
|
||||
|
||||
**Computed Properties:**
|
||||
|
||||
- `destinationIcon()`: Returns the appropriate icon name based on order type
|
||||
- `name()`: Returns recipient name (customer for delivery, branch name for pickup, or "Hugendubel Digital" for downloads)
|
||||
- `address()`: Returns the appropriate address object based on order type
|
||||
|
||||
### Type Definitions
|
||||
|
||||
#### `Address`
|
||||
|
||||
```ts
|
||||
type Address = {
|
||||
apartment?: string;
|
||||
careOf?: string;
|
||||
city?: string;
|
||||
country?: string;
|
||||
district?: string;
|
||||
info?: string;
|
||||
po?: string;
|
||||
region?: string;
|
||||
state?: string;
|
||||
street?: string;
|
||||
streetNumber?: string;
|
||||
zipCode?: string;
|
||||
};
|
||||
```
|
||||
|
||||
#### `Branch`
|
||||
|
||||
```ts
|
||||
type Branch = {
|
||||
name?: string;
|
||||
address?: Address;
|
||||
};
|
||||
```
|
||||
|
||||
#### `ShippingAddress`
|
||||
|
||||
```ts
|
||||
type ShippingAddress = {
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
address?: Address;
|
||||
};
|
||||
```
|
||||
|
||||
#### `EstimatedDelivery`
|
||||
|
||||
```ts
|
||||
type EstimatedDelivery = {
|
||||
start: string; // ISO date string
|
||||
stop: string | null; // ISO date string or null for single-day delivery
|
||||
};
|
||||
```
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Basic Delivery Order
|
||||
|
||||
```html
|
||||
<shared-order-destination
|
||||
[orderType]="OrderTypeFeature.Delivery"
|
||||
[shippingAddress]="{
|
||||
firstName: 'Max',
|
||||
lastName: 'Mustermann',
|
||||
address: {
|
||||
street: 'Musterstraße',
|
||||
streetNumber: '42',
|
||||
city: 'München',
|
||||
zipCode: '80331'
|
||||
}
|
||||
}"
|
||||
/>
|
||||
```
|
||||
|
||||
### Pickup Order with Branch
|
||||
|
||||
```html
|
||||
<shared-order-destination
|
||||
[orderType]="OrderTypeFeature.Pickup"
|
||||
[branch]="{
|
||||
name: 'Hugendubel München Stachus',
|
||||
address: {
|
||||
street: 'Karlsplatz',
|
||||
streetNumber: '11-13',
|
||||
city: 'München',
|
||||
zipCode: '80335'
|
||||
}
|
||||
}"
|
||||
/>
|
||||
```
|
||||
|
||||
### Digital Download
|
||||
|
||||
```html
|
||||
<shared-order-destination
|
||||
[orderType]="OrderTypeFeature.Download"
|
||||
/>
|
||||
```
|
||||
|
||||
### With Estimated Delivery Date Range
|
||||
|
||||
```html
|
||||
<shared-order-destination
|
||||
[orderType]="OrderTypeFeature.Delivery"
|
||||
[estimatedDelivery]="{
|
||||
start: '2025-11-27',
|
||||
stop: '2025-11-29'
|
||||
}"
|
||||
/>
|
||||
```
|
||||
|
||||
### With Underlined Order Type
|
||||
|
||||
```html
|
||||
<shared-order-destination
|
||||
[orderType]="OrderTypeFeature.Delivery"
|
||||
[shippingAddress]="address"
|
||||
[underline]="true"
|
||||
/>
|
||||
```
|
||||
|
||||
### In-Store Order
|
||||
|
||||
```html
|
||||
<shared-order-destination
|
||||
[orderType]="OrderTypeFeature.InStore"
|
||||
[branch]="{
|
||||
name: 'Hugendubel Berlin Tauentzienstraße',
|
||||
address: {
|
||||
street: 'Tauentzienstraße',
|
||||
streetNumber: '13',
|
||||
city: 'Berlin',
|
||||
zipCode: '10789'
|
||||
}
|
||||
}"
|
||||
/>
|
||||
```
|
||||
|
||||
## Order Type Icons
|
||||
|
||||
The component automatically selects icons based on order type:
|
||||
|
||||
| Order Type | Icon | Display |
|
||||
|------------|------|---------|
|
||||
| `Delivery` | `isaDeliveryVersand` | Shipping truck icon |
|
||||
| `Pickup` | `isaDeliveryRuecklage2` | Pickup box icon |
|
||||
| `InStore` | `isaDeliveryRuecklage1` | Store icon |
|
||||
| `Download` | `isaDeliveryDownload` | Download icon |
|
||||
| `B2BShipping` | `isaDeliveryVersand` | Shipping truck icon |
|
||||
| `DigitalShipping` | `isaDeliveryVersand` | Shipping truck icon |
|
||||
|
||||
## Display Logic
|
||||
|
||||
The component intelligently displays information based on order type:
|
||||
|
||||
**Delivery/B2BShipping/DigitalShipping:**
|
||||
- Shows customer name (firstName + lastName)
|
||||
- Displays shipping address
|
||||
|
||||
**Pickup/InStore:**
|
||||
- Shows branch name
|
||||
- Displays branch address
|
||||
|
||||
**Download:**
|
||||
- Shows "Hugendubel Digital"
|
||||
- No address displayed
|
||||
|
||||
**Estimated Delivery (fallback when no name/address):**
|
||||
- Date range: "Zustellung zwischen [start date] und [stop date]"
|
||||
- Single date: "Zustellung am [date]"
|
||||
- Format: `EEE, dd.MM.` (e.g., "Mo, 27.11.")
|
||||
|
||||
## Accessibility Features
|
||||
|
||||
- **Semantic HTML**: Uses proper role attributes (`role="status"`, `role="region"`)
|
||||
- **ARIA Labels**: Provides context for screen readers
|
||||
- **Icon Hiding**: Icons are hidden from screen readers (`aria-hidden="true"`)
|
||||
- **E2E Attributes**: Includes `data-what` attributes for testing:
|
||||
- `order-type-indicator`
|
||||
- `destination-details`
|
||||
- `recipient-name`
|
||||
- `destination-address`
|
||||
- `estimated-delivery`
|
||||
|
||||
## Styling
|
||||
|
||||
The component uses Tailwind CSS with ISA design system tokens:
|
||||
|
||||
- **Layout**: Flexbox with 2-unit gap, grows to fill available space
|
||||
- **Typography**:
|
||||
- Order type: `isa-text-body-2-bold`, `text-isa-secondary-900`
|
||||
- Address details: `isa-text-body-2-regular`, `text-isa-neutral-600`
|
||||
- **Address Container**: Line-clamps to 2 lines with text ellipsis for overflow
|
||||
|
||||
## Dependencies
|
||||
|
||||
### Internal Dependencies
|
||||
- `@isa/shared/address` - InlineAddressComponent for formatted address display
|
||||
- `@isa/icons` - Icon set (isaDeliveryVersand, isaDeliveryRuecklage1/2, isaDeliveryDownload)
|
||||
- `@isa/common/data-access` - OrderTypeFeature enum
|
||||
- `@isa/core/logging` - Logging infrastructure
|
||||
|
||||
### External Dependencies
|
||||
- `@angular/core` - Angular framework
|
||||
- `@angular/common` - DatePipe for date formatting
|
||||
- `@angular/cdk/coercion` - Boolean property coercion
|
||||
- `@ng-icons/core` - Icon display component
|
||||
|
||||
## Testing
|
||||
|
||||
The component includes comprehensive unit tests covering:
|
||||
- Icon selection for all order types
|
||||
- Name computation for different scenarios
|
||||
- Address selection based on order type
|
||||
- Branch and shipping address handling
|
||||
|
||||
Run tests:
|
||||
```bash
|
||||
nx test shared-delivery
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
**Library Type:** UI Component Library
|
||||
**Nx Tags:** `skip:ci`
|
||||
**Project Type:** Angular Library
|
||||
**Prefix:** `lib`
|
||||
|
||||
@@ -1,169 +1,232 @@
|
||||
# Scanner Library
|
||||
# @isa/shared/scanner
|
||||
|
||||
Enterprise-grade barcode scanning library for ISA-Frontend using the Scandit SDK, providing mobile barcode scanning capabilities for iOS and Android platforms.
|
||||
|
||||
## Overview
|
||||
|
||||
The `@isa/shared/scanner` library provides enterprise-grade barcode scanning capabilities for the ISA application using the [Scandit SDK](https://www.scandit.com/). It offers a complete, production-ready solution for mobile barcode scanning with comprehensive support for multiple symbologies, platform detection (iOS/Android), ready-state management, and seamless integration with Angular forms and reactive patterns.
|
||||
The `@isa/shared/scanner` library provides a complete, production-ready solution for mobile barcode scanning with comprehensive support for multiple symbologies, platform detection, ready-state management, and seamless integration with Angular's reactive patterns. Built on the [Scandit SDK](https://www.scandit.com/), it offers ready-to-use components, directives, and services for implementing barcode scanning workflows throughout the ISA application.
|
||||
|
||||
**Key Use Cases:**
|
||||
- Barcode scanning for product lookup and inventory management
|
||||
- Product lookup and inventory management
|
||||
- Order processing and verification
|
||||
- Returns and remission workflows
|
||||
- Asset tracking and identification
|
||||
- QR code scanning for digital workflows
|
||||
|
||||
**Type:** Shared component library with services, directives, and ready-to-use UI components
|
||||
**Type:** Shared component library (UI + Services)
|
||||
|
||||
## Installation
|
||||
|
||||
```ts
|
||||
import {
|
||||
ScannerService,
|
||||
ScannerButtonComponent,
|
||||
ScannerReadyDirective
|
||||
} from '@isa/shared/scanner';
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
- Barcode scanning with support for multiple symbologies:
|
||||
- EAN8
|
||||
- EAN13/UPCA
|
||||
- UPCE
|
||||
- Code128
|
||||
- Code39
|
||||
- Code93
|
||||
- Interleaved 2 of 5
|
||||
- QR Code
|
||||
- Platform detection (iOS/Android support)
|
||||
- Ready-state detection with conditional rendering
|
||||
- Form control integration via output bindings
|
||||
- Configuration through injection tokens
|
||||
- Error handling for unsupported platforms
|
||||
- **Multiple Symbologies:** EAN-8, EAN-13/UPC-A, UPC-E, Code 128, Code 39, Code 93, Interleaved 2 of 5, QR Code
|
||||
- **Platform Detection:** Automatic iOS/Android detection with unsupported platform handling
|
||||
- **Ready-State Management:** Reactive signals for scanner initialization status
|
||||
- **Overlay UI:** Full-screen camera overlay with CDK integration
|
||||
- **Form Integration:** Output bindings for Angular forms
|
||||
- **Configurable:** Injection tokens for license key, library location, and symbologies
|
||||
- **TypeScript:** Full type safety with Zod schema validation
|
||||
- **Logging:** Integrated with `@isa/core/logging` for debugging
|
||||
- **E2E Testing:** E2E attributes for automated testing (`data-which="scan-button"`)
|
||||
|
||||
## Components and Directives
|
||||
## API Reference
|
||||
|
||||
### ScannerButtonComponent
|
||||
### Services
|
||||
|
||||
A button component that integrates with the scanner service to trigger barcode scanning. It implements `OnDestroy` to properly clean up resources.
|
||||
#### `ScannerService`
|
||||
|
||||
Core service that manages barcode scanning functionality using the Scandit SDK.
|
||||
|
||||
**Properties:**
|
||||
|
||||
- `ready: Signal<boolean>` - Computed signal indicating whether the scanner is initialized and ready to use. Automatically triggers configuration on first access.
|
||||
|
||||
**Methods:**
|
||||
|
||||
- `configure(): Promise<void>` - Manually configure the Scandit SDK. Called automatically by `open()` and the `ready` signal. Handles platform compatibility checks and license validation.
|
||||
- `open(options?: ScannerInputs): Promise<string | null>` - Opens the scanner overlay interface and returns a promise that resolves to the scanned barcode value. Returns `null` if the user cancels scanning.
|
||||
|
||||
**Configuration Options (`ScannerInputs`):**
|
||||
|
||||
```ts
|
||||
type ScannerInputs = {
|
||||
symbologies?: Symbology[]; // Override default barcode types
|
||||
abortSignal?: AbortSignal; // Cancel scanning operation
|
||||
};
|
||||
```
|
||||
|
||||
**Scanner Status:**
|
||||
|
||||
The service tracks initialization through the following states:
|
||||
- `None` - Initial state, not yet initialized
|
||||
- `Initializing` - Configuration in progress
|
||||
- `Ready` - Scanner fully initialized and ready for use
|
||||
- `Error` - Initialization failed
|
||||
|
||||
**Platform Support:**
|
||||
|
||||
Only iOS and Android platforms are supported. Attempting to use the scanner on unsupported platforms will log a warning and prevent initialization. The service throws `PlatformNotSupportedError` for unsupported platforms.
|
||||
|
||||
**Default Symbologies:**
|
||||
|
||||
The scanner recognizes the following barcode formats by default:
|
||||
- EAN-8
|
||||
- EAN-13 / UPC-A
|
||||
- UPC-E
|
||||
- Code 128
|
||||
- Code 39
|
||||
- Code 93
|
||||
- Interleaved 2 of 5
|
||||
- QR Code
|
||||
|
||||
### Components
|
||||
|
||||
#### `ScannerButtonComponent`
|
||||
|
||||
A ready-to-use button component that triggers the barcode scanner when clicked. Automatically handles scanner lifecycle and cleanup.
|
||||
|
||||
**Selector:** `shared-scanner-button`
|
||||
|
||||
**Inputs:**
|
||||
|
||||
- `size: InputSignal<IconButtonSize>` - Button size (default: `'large'`)
|
||||
- `disabled: ModelSignal<boolean>` - Whether the button is disabled (default: `false`)
|
||||
|
||||
**Outputs:**
|
||||
|
||||
- `scan: OutputEmitterRef<string | null>` - Emits the scanned barcode value or `null` if the user cancels
|
||||
|
||||
**Features:**
|
||||
|
||||
- Only appears when scanner is ready
|
||||
- Can be disabled through binding
|
||||
- Configurable size
|
||||
- Emits scanned value through output binding
|
||||
- Includes E2E testing attribute `data-which="scan-button"`
|
||||
- Only visible when scanner is ready (uses `*sharedScannerReady` directive internally)
|
||||
- Opens full-screen scanner overlay on click
|
||||
- Automatically aborts scanning operations on component destroy
|
||||
- Includes E2E testing attribute: `data-which="scan-button"`
|
||||
- Primary color styling with scanner icon
|
||||
|
||||
**Usage Example:**
|
||||
**Lifecycle:**
|
||||
|
||||
```typescript
|
||||
import { ScannerButtonComponent } from '@isa/core/scanner';
|
||||
- Implements `OnDestroy` to abort any in-progress scanning operations
|
||||
- Uses `AbortController` for proper cleanup
|
||||
|
||||
@Component({
|
||||
selector: 'app-my-component',
|
||||
template: `
|
||||
<shared-scanner-button
|
||||
[disabled]="isDisabled"
|
||||
[size]="'large'"
|
||||
(scan)="onScan($event)"
|
||||
>
|
||||
</shared-scanner-button>
|
||||
`,
|
||||
imports: [ScannerButtonComponent],
|
||||
standalone: true,
|
||||
})
|
||||
export class MyComponent {
|
||||
isDisabled = false;
|
||||
#### `ScannerComponent`
|
||||
|
||||
onScan(value: string | null) {
|
||||
console.log('Scanned barcode:', value);
|
||||
}
|
||||
Internal component that renders the camera view and handles barcode detection. Used by `ScannerService` via CDK overlay and typically not used directly in application code.
|
||||
|
||||
**Selector:** `shared-scanner`
|
||||
|
||||
**Inputs:**
|
||||
|
||||
- `symbologies: InputSignal<Symbology[]>` - Array of barcode types to detect
|
||||
|
||||
**Outputs:**
|
||||
|
||||
- `scan: OutputEmitterRef<string>` - Emits detected barcode data
|
||||
|
||||
**Features:**
|
||||
|
||||
- Full-screen camera view with close button
|
||||
- Progress indicator during initialization
|
||||
- Zone-aware event handling for optimal change detection
|
||||
- Automatic camera lifecycle management (on/off)
|
||||
- Cleanup on destroy to prevent memory leaks
|
||||
|
||||
### Directives
|
||||
|
||||
#### `ScannerReadyDirective`
|
||||
|
||||
Structural directive that conditionally renders content based on scanner ready state. Similar to `*ngIf`, but specifically tied to scanner initialization status.
|
||||
|
||||
**Selector:** `*sharedScannerReady`
|
||||
|
||||
**Inputs:**
|
||||
|
||||
- `scannerReadyElse: InputSignal<TemplateRef<unknown> | undefined>` - Optional template to show when scanner is not ready (similar to `*ngIf` else template)
|
||||
|
||||
**Features:**
|
||||
|
||||
- Reactive to scanner status changes using Angular effects
|
||||
- Supports else template for fallback content
|
||||
- Automatically updates when scanner becomes ready
|
||||
- No manual subscription management required
|
||||
|
||||
### Injection Tokens
|
||||
|
||||
#### `SCANDIT_LICENSE`
|
||||
|
||||
Injection token for the Scandit license key.
|
||||
|
||||
**Default:** Retrieves from application config at `licence.scandit` path
|
||||
**Type:** `InjectionToken<string>`
|
||||
|
||||
#### `SCANDIT_LIBRARY_LOCATION`
|
||||
|
||||
Injection token for the Scandit SDK library location.
|
||||
|
||||
**Default:** `/scandit` relative to `document.baseURI`
|
||||
**Type:** `InjectionToken<string>`
|
||||
|
||||
#### `SCANDIT_DEFAULT_SYMBOLOGIES`
|
||||
|
||||
Injection token for the default barcode symbologies to scan.
|
||||
|
||||
**Default:** Array of common symbologies (EAN, UPC, Code128, Code39, Code93, ITF, QR)
|
||||
**Type:** `InjectionToken<Symbology[]>`
|
||||
|
||||
### Errors
|
||||
|
||||
#### `PlatformNotSupportedError`
|
||||
|
||||
Thrown when attempting to use the scanner on non-mobile platforms.
|
||||
|
||||
```ts
|
||||
export class PlatformNotSupportedError extends Error {
|
||||
constructor(); // Message: "ScannerService is only supported on iOS and Android platforms"
|
||||
}
|
||||
```
|
||||
|
||||
### ScannerReadyDirective
|
||||
**Behavior:**
|
||||
- Caught by `ScannerService.configure()` and logged as a warning
|
||||
- Prevents scanner initialization on unsupported platforms
|
||||
- Does not crash the application
|
||||
|
||||
A structural directive (`*sharedScannerReady`) that conditionally renders its content based on the scanner's ready state. Similar to `*ngIf`, but specifically tied to scanner readiness.
|
||||
## Usage Examples
|
||||
|
||||
**Features:**
|
||||
### Basic Programmatic Scanning
|
||||
|
||||
- Only renders content when the scanner is ready
|
||||
- Supports an optional else template for when the scanner is not ready
|
||||
- Uses Angular's effect system for reactive updates
|
||||
|
||||
**Usage Example:**
|
||||
|
||||
```typescript
|
||||
import { ScannerReadyDirective } from '@isa/core/scanner';
|
||||
|
||||
@Component({
|
||||
selector: 'app-my-component',
|
||||
template: `
|
||||
<div *sharedScannerReady>
|
||||
<!-- Content only shown when scanner is ready -->
|
||||
<p>Scanner is ready to use</p>
|
||||
<button (click)="startScanning()">Scan Now</button>
|
||||
</div>
|
||||
|
||||
<!-- Alternative with else template -->
|
||||
<div *sharedScannerReady="; else notReady">
|
||||
<p>Scanner is ready</p>
|
||||
</div>
|
||||
|
||||
<ng-template #notReady>
|
||||
<p>Scanner is not yet ready</p>
|
||||
<app-spinner></app-spinner>
|
||||
</ng-template>
|
||||
`,
|
||||
imports: [ScannerReadyDirective],
|
||||
standalone: true,
|
||||
})
|
||||
export class MyComponent {
|
||||
// Component logic
|
||||
}
|
||||
```
|
||||
|
||||
### ScannerComponent
|
||||
|
||||
Internal component used by ScannerService to render the camera view and process barcode scanning.
|
||||
|
||||
**Features:**
|
||||
|
||||
- Integrates with Scandit SDK
|
||||
- Handles camera setup and barcode detection
|
||||
- Emits scanned values
|
||||
- Includes a close button to cancel scanning
|
||||
|
||||
## Services
|
||||
|
||||
### ScannerService
|
||||
|
||||
Core service that provides barcode scanning functionality.
|
||||
|
||||
**Features:**
|
||||
|
||||
- Initializes and configures Scandit SDK
|
||||
- Checks platform compatibility
|
||||
- Manages scanner lifecycle
|
||||
- Provides a reactive `ready` signal
|
||||
- Handles scanning operations with proper cleanup
|
||||
|
||||
**Usage Example:**
|
||||
|
||||
```typescript
|
||||
import { ScannerService, ScannerInputs } from '@isa/core/scanner';
|
||||
```ts
|
||||
import { Component, inject } from '@angular/core';
|
||||
import { ScannerService } from '@isa/shared/scanner';
|
||||
|
||||
@Component({
|
||||
selector: 'app-my-component',
|
||||
selector: 'app-checkout',
|
||||
template: `
|
||||
<button (click)="scan()" [disabled]="!isReady()">Scan Barcode</button>
|
||||
<div *ngIf="result">Last Scan: {{ result }}</div>
|
||||
`,
|
||||
standalone: true,
|
||||
<button (click)="scanProduct()">Scan Product</button>
|
||||
@if (scannedCode) {
|
||||
<p>Scanned: {{ scannedCode }}</p>
|
||||
}
|
||||
`
|
||||
})
|
||||
export class MyComponent {
|
||||
private scannerService = inject(ScannerService);
|
||||
isReady = this.scannerService.ready;
|
||||
result: string | null = null;
|
||||
export class CheckoutComponent {
|
||||
#scannerService = inject(ScannerService);
|
||||
|
||||
async scan() {
|
||||
const options: ScannerInputs = {
|
||||
// Optional configuration
|
||||
// symbologies: [...] // Specify barcode types
|
||||
};
|
||||
scannedCode: string | null = null;
|
||||
|
||||
async scanProduct() {
|
||||
try {
|
||||
this.result = await this.scannerService.open(options);
|
||||
this.scannedCode = await this.#scannerService.open();
|
||||
if (this.scannedCode) {
|
||||
console.log('Product code:', this.scannedCode);
|
||||
// Process the scanned code (e.g., fetch product details)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Scanning failed:', error);
|
||||
}
|
||||
@@ -171,53 +234,381 @@ export class MyComponent {
|
||||
}
|
||||
```
|
||||
|
||||
## Configuration
|
||||
### Using Scanner Button Component
|
||||
|
||||
The scanner module uses injection tokens for configuration:
|
||||
```ts
|
||||
import { Component } from '@angular/core';
|
||||
import { ScannerButtonComponent } from '@isa/shared/scanner';
|
||||
|
||||
- `SCANDIT_LICENSE` - The Scandit license key (defaults to config value at 'licence.scandit')
|
||||
- `SCANDIT_LIBRARY_LOCATION` - The location of the Scandit library (defaults to '/scandit' relative to base URI)
|
||||
- `SCANDIT_DEFAULT_SYMBOLOGIES` - The default barcode symbologies to use
|
||||
@Component({
|
||||
selector: 'app-inventory',
|
||||
imports: [ScannerButtonComponent],
|
||||
template: `
|
||||
<h2>Inventory Lookup</h2>
|
||||
|
||||
**Custom Configuration Example:**
|
||||
<shared-scanner-button
|
||||
[size]="'large'"
|
||||
[disabled]="isProcessing"
|
||||
(scan)="onScan($event)"
|
||||
/>
|
||||
|
||||
```typescript
|
||||
import { SCANDIT_LICENSE, SCANDIT_LIBRARY_LOCATION } from '@isa/core/scanner';
|
||||
@if (lastScan) {
|
||||
<div class="scan-result">
|
||||
<p>Last scanned: {{ lastScan }}</p>
|
||||
<p>Product: {{ productName }}</p>
|
||||
</div>
|
||||
}
|
||||
`
|
||||
})
|
||||
export class InventoryComponent {
|
||||
lastScan: string | null = null;
|
||||
productName: string = '';
|
||||
isProcessing = false;
|
||||
|
||||
async onScan(code: string | null) {
|
||||
if (code) {
|
||||
this.lastScan = code;
|
||||
this.isProcessing = true;
|
||||
await this.processBarcode(code);
|
||||
this.isProcessing = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async processBarcode(code: string) {
|
||||
// Fetch product details from API
|
||||
const response = await fetch(`/api/products/${code}`);
|
||||
const product = await response.json();
|
||||
this.productName = product.name;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Scanning with Custom Symbologies
|
||||
|
||||
```ts
|
||||
import { Component, inject } from '@angular/core';
|
||||
import { ScannerService } from '@isa/shared/scanner';
|
||||
import { Symbology } from 'scandit-web-datacapture-barcode';
|
||||
|
||||
@NgModule({
|
||||
@Component({
|
||||
selector: 'app-qr-scanner',
|
||||
template: `
|
||||
<button (click)="scanQRCode()">Scan QR Code Only</button>
|
||||
<button (click)="scanEAN()">Scan Product Barcode</button>
|
||||
`
|
||||
})
|
||||
export class QRScannerComponent {
|
||||
#scannerService = inject(ScannerService);
|
||||
|
||||
async scanQRCode() {
|
||||
// Only scan QR codes
|
||||
const result = await this.#scannerService.open({
|
||||
symbologies: [Symbology.QR]
|
||||
});
|
||||
|
||||
if (result) {
|
||||
this.handleQRCode(result);
|
||||
}
|
||||
}
|
||||
|
||||
async scanEAN() {
|
||||
// Only scan EAN barcodes
|
||||
const result = await this.#scannerService.open({
|
||||
symbologies: [Symbology.EAN8, Symbology.EAN13UPCA]
|
||||
});
|
||||
|
||||
if (result) {
|
||||
this.handleProductCode(result);
|
||||
}
|
||||
}
|
||||
|
||||
private handleQRCode(data: string) {
|
||||
console.log('QR Code:', data);
|
||||
}
|
||||
|
||||
private handleProductCode(code: string) {
|
||||
console.log('Product Code:', code);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Using Scanner Ready Directive
|
||||
|
||||
```ts
|
||||
import { Component } from '@angular/core';
|
||||
import { ScannerButtonComponent, ScannerReadyDirective } from '@isa/shared/scanner';
|
||||
|
||||
@Component({
|
||||
selector: 'app-product-lookup',
|
||||
imports: [ScannerButtonComponent, ScannerReadyDirective],
|
||||
template: `
|
||||
<div *sharedScannerReady="; else scannerNotReady">
|
||||
<h2>Scan Product Barcode</h2>
|
||||
<p>Point your camera at a barcode to scan</p>
|
||||
<shared-scanner-button (scan)="onScan($event)" />
|
||||
</div>
|
||||
|
||||
<ng-template #scannerNotReady>
|
||||
<div class="fallback">
|
||||
<p>Camera scanner is not available on this device</p>
|
||||
<p>Please enter the barcode manually:</p>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Enter barcode"
|
||||
(input)="onManualEntry($event)"
|
||||
/>
|
||||
</div>
|
||||
</ng-template>
|
||||
`
|
||||
})
|
||||
export class ProductLookupComponent {
|
||||
onScan(code: string | null) {
|
||||
if (code) {
|
||||
this.lookupProduct(code);
|
||||
}
|
||||
}
|
||||
|
||||
onManualEntry(event: Event) {
|
||||
const input = event.target as HTMLInputElement;
|
||||
if (input.value.length >= 8) {
|
||||
this.lookupProduct(input.value);
|
||||
}
|
||||
}
|
||||
|
||||
private lookupProduct(barcode: string) {
|
||||
console.log('Looking up product:', barcode);
|
||||
// Fetch product by barcode
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Cancelling a Scan Operation
|
||||
|
||||
```ts
|
||||
import { Component, inject, OnDestroy } from '@angular/core';
|
||||
import { ScannerService } from '@isa/shared/scanner';
|
||||
|
||||
@Component({
|
||||
selector: 'app-advanced-scanner',
|
||||
template: `
|
||||
<button (click)="startScan()" [disabled]="isScanning">
|
||||
{{ isScanning ? 'Scanning...' : 'Start Scan' }}
|
||||
</button>
|
||||
<button (click)="cancelScan()" [disabled]="!isScanning">
|
||||
Cancel
|
||||
</button>
|
||||
|
||||
@if (errorMessage) {
|
||||
<p class="error">{{ errorMessage }}</p>
|
||||
}
|
||||
`
|
||||
})
|
||||
export class AdvancedScannerComponent implements OnDestroy {
|
||||
#scannerService = inject(ScannerService);
|
||||
#abortController = new AbortController();
|
||||
|
||||
isScanning = false;
|
||||
errorMessage = '';
|
||||
|
||||
async startScan() {
|
||||
this.isScanning = true;
|
||||
this.errorMessage = '';
|
||||
|
||||
try {
|
||||
const result = await this.#scannerService.open({
|
||||
abortSignal: this.#abortController.signal
|
||||
});
|
||||
|
||||
if (result) {
|
||||
console.log('Scanned:', result);
|
||||
} else {
|
||||
console.log('Scan was cancelled by user');
|
||||
}
|
||||
} catch (error) {
|
||||
this.errorMessage = 'Scan was cancelled or failed';
|
||||
console.error('Scan error:', error);
|
||||
} finally {
|
||||
this.isScanning = false;
|
||||
}
|
||||
}
|
||||
|
||||
cancelScan() {
|
||||
this.#abortController.abort();
|
||||
this.#abortController = new AbortController(); // Create new controller for next scan
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.#abortController.abort();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Checking Scanner Readiness
|
||||
|
||||
```ts
|
||||
import { Component, inject, effect } from '@angular/core';
|
||||
import { ScannerService } from '@isa/shared/scanner';
|
||||
|
||||
@Component({
|
||||
selector: 'app-scanner-status',
|
||||
template: `
|
||||
<div class="status-indicator">
|
||||
@if (scannerReady()) {
|
||||
<span class="ready">Scanner Ready</span>
|
||||
} @else {
|
||||
<span class="initializing">Initializing Scanner...</span>
|
||||
}
|
||||
</div>
|
||||
`
|
||||
})
|
||||
export class ScannerStatusComponent {
|
||||
#scannerService = inject(ScannerService);
|
||||
|
||||
scannerReady = this.#scannerService.ready;
|
||||
|
||||
constructor() {
|
||||
effect(() => {
|
||||
if (this.scannerReady()) {
|
||||
console.log('Scanner initialized successfully');
|
||||
// Perform actions when scanner becomes ready
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Basic Configuration
|
||||
|
||||
The library requires a Scandit license key configured in your application config:
|
||||
|
||||
```json
|
||||
{
|
||||
"licence": {
|
||||
"scandit": "YOUR_SCANDIT_LICENSE_KEY"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The Scandit SDK library files must be available at `/scandit/` relative to your application's base URI.
|
||||
|
||||
### Custom Configuration via Injection Tokens
|
||||
|
||||
Override default configuration by providing custom values:
|
||||
|
||||
```ts
|
||||
import {
|
||||
SCANDIT_LICENSE,
|
||||
SCANDIT_LIBRARY_LOCATION,
|
||||
SCANDIT_DEFAULT_SYMBOLOGIES
|
||||
} from '@isa/shared/scanner';
|
||||
import { Symbology } from 'scandit-web-datacapture-barcode';
|
||||
|
||||
// In your providers array (e.g., app.config.ts):
|
||||
export const appConfig: ApplicationConfig = {
|
||||
providers: [
|
||||
// Custom license key
|
||||
{
|
||||
provide: SCANDIT_LICENSE,
|
||||
useValue: 'YOUR-SCANDIT-LICENSE-KEY',
|
||||
useValue: 'YOUR-CUSTOM-LICENSE-KEY'
|
||||
},
|
||||
// Custom library location
|
||||
// Custom library location (e.g., CDN)
|
||||
{
|
||||
provide: SCANDIT_LIBRARY_LOCATION,
|
||||
useValue: 'https://cdn.example.com/scandit/',
|
||||
useValue: 'https://cdn.example.com/scandit/'
|
||||
},
|
||||
],
|
||||
})
|
||||
export class AppModule {}
|
||||
// Custom default symbologies (only QR and Code128)
|
||||
{
|
||||
provide: SCANDIT_DEFAULT_SYMBOLOGIES,
|
||||
useValue: [Symbology.QR, Symbology.Code128]
|
||||
}
|
||||
]
|
||||
};
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
### Core Dependencies
|
||||
|
||||
- **@angular/core** - Angular framework (signals, effects, DI)
|
||||
- **@angular/cdk** - CDK platform detection and overlay
|
||||
- **scandit-web-datacapture-core** - Scandit SDK core functionality
|
||||
- **scandit-web-datacapture-barcode** - Scandit barcode scanning module
|
||||
- **zod** - Configuration schema validation
|
||||
|
||||
### ISA Dependencies
|
||||
|
||||
- **@isa/core/config** - Application configuration management
|
||||
- **@isa/core/logging** - Logging utilities with factory pattern
|
||||
- **@isa/ui/buttons** - Icon button component
|
||||
- **@isa/icons** - Icon library (scanner and close icons)
|
||||
|
||||
## Error Handling
|
||||
|
||||
The scanner library includes error handling for various scenarios:
|
||||
The scanner library provides comprehensive error handling:
|
||||
|
||||
- `PlatformNotSupportedError` is thrown when the scanner is used on unsupported platforms
|
||||
- Configuration errors are logged and propagated
|
||||
- Aborted scan operations are handled gracefully
|
||||
### Platform Compatibility
|
||||
|
||||
## Requirements
|
||||
```ts
|
||||
// Automatically handled by ScannerService
|
||||
if (!platform.IOS && !platform.ANDROID) {
|
||||
// Logs warning and prevents initialization
|
||||
// Does not crash the application
|
||||
}
|
||||
```
|
||||
|
||||
- The Scandit SDK must be properly installed and configured
|
||||
- Requires a valid Scandit license key
|
||||
- Currently supports iOS and Android platforms
|
||||
### Configuration Errors
|
||||
|
||||
```ts
|
||||
try {
|
||||
await scannerService.configure();
|
||||
} catch (error) {
|
||||
// Configuration errors are logged and set status to 'Error'
|
||||
console.error('Scanner configuration failed:', error);
|
||||
}
|
||||
```
|
||||
|
||||
### Scan Operation Errors
|
||||
|
||||
```ts
|
||||
try {
|
||||
const result = await scannerService.open();
|
||||
} catch (error) {
|
||||
// Handle abort or configuration errors
|
||||
if (error.message === 'Scanner aborted') {
|
||||
console.log('User cancelled scanning');
|
||||
} else {
|
||||
console.error('Scanning failed:', error);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Architecture Notes
|
||||
|
||||
- **Signals & Effects:** Uses Angular signals for reactive state management and effects for side effect handling
|
||||
- **CDK Overlay:** Integrates with Angular CDK Overlay for full-screen scanner UI with backdrop and positioning
|
||||
- **Camera Lifecycle:** Automatic camera management (on/off state) with proper cleanup
|
||||
- **Zone Management:** Zone-aware event handling for optimal change detection performance
|
||||
- **Standalone:** All components and directives are standalone for easy tree-shaking
|
||||
- **Logging:** Comprehensive logging with `@isa/core/logging` factory pattern
|
||||
- **Memory Safety:** Proper cleanup in `OnDestroy` lifecycle hooks to prevent memory leaks
|
||||
|
||||
## Platform Requirements
|
||||
|
||||
- **Supported Platforms:** iOS and Android only
|
||||
- **Scandit License:** Valid Scandit SDK license key required
|
||||
- **Camera Permissions:** Application must request and obtain camera permissions
|
||||
- **Library Files:** Scandit SDK files must be accessible at configured location
|
||||
|
||||
## E2E Testing
|
||||
|
||||
The scanner components include E2E testing attributes for easier selection in automated tests:
|
||||
The scanner components include E2E testing attributes for automated test selection:
|
||||
|
||||
- ScannerButton includes `data-which="scan-button"` for E2E test selection
|
||||
- `ScannerButtonComponent` includes `data-which="scan-button"` attribute
|
||||
- Use this attribute to locate and interact with the scanner button in E2E tests
|
||||
|
||||
```ts
|
||||
// Example E2E test
|
||||
await page.click('[data-which="scan-button"]');
|
||||
```
|
||||
|
||||
@@ -1,7 +1,132 @@
|
||||
# format-name
|
||||
# @isa/utils/format-name
|
||||
|
||||
This library was generated with [Nx](https://nx.dev).
|
||||
A utility library for consistently formatting person and organisation names across the ISA-Frontend application.
|
||||
|
||||
## Running unit tests
|
||||
## Overview
|
||||
|
||||
Run `nx test format-name` to execute the unit tests.
|
||||
This library provides a single utility function `formatName` that standardizes the display of names by combining first name, last name, and organisation name components according to a consistent business rule. The function handles optional parameters gracefully and filters out empty or undefined values to produce clean, readable name strings.
|
||||
|
||||
The formatting follows the convention:
|
||||
- Personal names are displayed as "LastName FirstName"
|
||||
- Organisation names are separated from personal names with " - "
|
||||
- Any combination of components can be provided
|
||||
- Empty or undefined values are automatically excluded from the output
|
||||
|
||||
## Installation
|
||||
|
||||
```ts
|
||||
import { formatName } from '@isa/utils/format-name';
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### Functions
|
||||
|
||||
#### `formatName(params): string`
|
||||
|
||||
Formats a name by combining first name, last name, and organisation name according to business rules.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `params` | `object` | Yes | An object containing the name components |
|
||||
| `params.firstName` | `string` | No | The person's first name |
|
||||
| `params.lastName` | `string` | No | The person's last name |
|
||||
| `params.organisationName` | `string` | No | The organisation name |
|
||||
|
||||
**Returns:** `string` - The formatted name string
|
||||
|
||||
**Formatting Rules:**
|
||||
- Names are formatted as "LastName FirstName"
|
||||
- Organisation name is separated from the personal name with " - "
|
||||
- Empty or undefined values are filtered out
|
||||
- If all values are empty/undefined, returns an empty string
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Format a person with first and last name
|
||||
|
||||
```ts
|
||||
const result = formatName({
|
||||
firstName: 'John',
|
||||
lastName: 'Doe'
|
||||
});
|
||||
// Returns: "Doe John"
|
||||
```
|
||||
|
||||
### Format a person with organisation
|
||||
|
||||
```ts
|
||||
const result = formatName({
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
organisationName: 'Acme Corp'
|
||||
});
|
||||
// Returns: "Acme Corp - Doe John"
|
||||
```
|
||||
|
||||
### Format organisation only
|
||||
|
||||
```ts
|
||||
const result = formatName({
|
||||
organisationName: 'Acme Corp'
|
||||
});
|
||||
// Returns: "Acme Corp"
|
||||
```
|
||||
|
||||
### Format with partial personal name
|
||||
|
||||
```ts
|
||||
// Only last name
|
||||
const result1 = formatName({
|
||||
lastName: 'Doe'
|
||||
});
|
||||
// Returns: "Doe"
|
||||
|
||||
// Only first name
|
||||
const result2 = formatName({
|
||||
firstName: 'John'
|
||||
});
|
||||
// Returns: "John"
|
||||
|
||||
// Organisation with partial personal name
|
||||
const result3 = formatName({
|
||||
firstName: 'John',
|
||||
organisationName: 'Acme Corp'
|
||||
});
|
||||
// Returns: "Acme Corp - John"
|
||||
```
|
||||
|
||||
### Handle empty values
|
||||
|
||||
```ts
|
||||
// Empty strings are filtered out
|
||||
const result1 = formatName({
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
organisationName: ''
|
||||
});
|
||||
// Returns: ""
|
||||
|
||||
// Undefined values are filtered out
|
||||
const result2 = formatName({});
|
||||
// Returns: ""
|
||||
```
|
||||
|
||||
## Use Cases
|
||||
|
||||
This utility is commonly used in:
|
||||
- Customer management interfaces (CRM)
|
||||
- Order processing displays
|
||||
- User profile presentations
|
||||
- Report generation
|
||||
- Any UI component that needs to display person or organisation names consistently
|
||||
|
||||
## Testing
|
||||
|
||||
The library includes comprehensive test coverage for all formatting scenarios. Run tests with:
|
||||
|
||||
```bash
|
||||
nx test utils-format-name
|
||||
```
|
||||
|
||||
@@ -1,166 +1,253 @@
|
||||
# utils-positive-integer-input
|
||||
# @isa/utils/positive-integer-input
|
||||
|
||||
An Angular directive that ensures only positive integers can be entered into number input fields.
|
||||
An Angular directive that ensures only positive integers can be entered into number input fields through keyboard blocking, paste sanitization, and input validation.
|
||||
|
||||
## Features
|
||||
## Overview
|
||||
|
||||
- ✅ Blocks invalid characters during keyboard input (`.`, `,`, `-`, `+`, `e`, `E`)
|
||||
- ✅ Sanitizes pasted content to extract only positive integers
|
||||
- ✅ Handles all input methods (typing, paste, drag & drop)
|
||||
- ✅ Removes leading zeros automatically
|
||||
- ✅ Works seamlessly with Angular forms (`ngModel`, `formControl`)
|
||||
- ✅ Standalone directive - easy to import
|
||||
The `PositiveIntegerInputDirective` provides three layers of protection to prevent invalid input in number fields:
|
||||
|
||||
1. **Keyboard input blocking** - Prevents invalid characters from being typed
|
||||
2. **Paste sanitization** - Cleans pasted content to extract only positive integers
|
||||
3. **General input validation** - Catches any other input methods (drag & drop, autofill, voice input, etc.)
|
||||
|
||||
This directive is particularly useful for scenarios where you need to enforce strictly positive integer values, such as:
|
||||
- Quantity inputs in shopping carts
|
||||
- Age or count fields
|
||||
- Point or score inputs
|
||||
- Any numeric field that should not accept decimals, negatives, or special characters
|
||||
|
||||
### Key Behaviors
|
||||
|
||||
**Blocked characters:** `.`, `,`, `-`, `+`, `e`, `E`, and all other non-digit characters
|
||||
|
||||
**Important notes:**
|
||||
- This directive removes ALL non-digit characters, so `1.58` becomes `158`, not `1` or `2`
|
||||
- Leading zeros are automatically removed (`007` becomes `7`)
|
||||
- A single `0` input results in an empty field
|
||||
- Works seamlessly with Angular forms (`ngModel`, `formControl`, `formControlName`)
|
||||
|
||||
## Installation
|
||||
|
||||
The directive is available through the `@isa/utils/positive-integer-input` package.
|
||||
Import the directive in your component:
|
||||
|
||||
## Usage
|
||||
```ts
|
||||
import { PositiveIntegerInputDirective } from '@isa/utils/positive-integer-input';
|
||||
|
||||
@Component({
|
||||
selector: 'app-my-component',
|
||||
standalone: true,
|
||||
imports: [PositiveIntegerInputDirective],
|
||||
// ...
|
||||
})
|
||||
export class MyComponent {}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### PositiveIntegerInputDirective
|
||||
|
||||
**Selector:** `input[type="number"][positiveIntegerInput]`
|
||||
|
||||
**Standalone:** Yes
|
||||
|
||||
A standalone Angular directive that attaches to `input[type="number"]` elements and enforces positive integer input.
|
||||
|
||||
#### Host Listeners
|
||||
|
||||
| Event | Handler | Description |
|
||||
|-------|---------|-------------|
|
||||
| `keydown` | `onKeyDown(event)` | Blocks invalid characters (`.`, `,`, `-`, `+`, `e`, `E`) before they can be entered |
|
||||
| `paste` | `onPaste(event)` | Intercepts paste events, sanitizes clipboard content, and inserts only positive integers |
|
||||
| `input` | `onInput(event)` | Catch-all for other input methods (drag & drop, autofill, voice input, IME) |
|
||||
|
||||
#### Private Methods
|
||||
|
||||
**`sanitizeInput(value: string): string`**
|
||||
|
||||
Sanitizes a string to contain only positive integers by:
|
||||
1. Removing all non-digit characters using regex `\D`
|
||||
2. Removing leading zeros to prevent parsing issues
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Basic Usage
|
||||
|
||||
Simply add the `positiveIntegerInput` directive to any `<input type="number">` element:
|
||||
|
||||
```html
|
||||
<input type="number" positiveIntegerInput />
|
||||
```
|
||||
|
||||
### With Angular Forms
|
||||
### With ngModel (Two-way Binding)
|
||||
|
||||
```html
|
||||
<!-- With ngModel -->
|
||||
<input
|
||||
type="number"
|
||||
positiveIntegerInput
|
||||
[(ngModel)]="points"
|
||||
placeholder="Enter points"
|
||||
<input
|
||||
type="number"
|
||||
positiveIntegerInput
|
||||
[(ngModel)]="quantity"
|
||||
placeholder="Enter quantity"
|
||||
/>
|
||||
```
|
||||
|
||||
<!-- With Reactive Forms -->
|
||||
<input
|
||||
type="number"
|
||||
positiveIntegerInput
|
||||
```ts
|
||||
export class MyComponent {
|
||||
quantity: number | undefined;
|
||||
}
|
||||
```
|
||||
|
||||
### With Reactive Forms (FormControl)
|
||||
|
||||
```html
|
||||
<input
|
||||
type="number"
|
||||
positiveIntegerInput
|
||||
[formControl]="pointsControl"
|
||||
placeholder="Enter points"
|
||||
/>
|
||||
```
|
||||
|
||||
### Complete Example
|
||||
```ts
|
||||
import { FormControl } from '@angular/forms';
|
||||
|
||||
```typescript
|
||||
import { Component, signal } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { PositiveIntegerInputDirective } from '@isa/utils/positive-integer-input';
|
||||
|
||||
@Component({
|
||||
selector: 'app-booking',
|
||||
standalone: true,
|
||||
imports: [FormsModule, PositiveIntegerInputDirective],
|
||||
template: `
|
||||
<input
|
||||
type="number"
|
||||
positiveIntegerInput
|
||||
[(ngModel)]="points"
|
||||
placeholder="Punkte"
|
||||
min="1"
|
||||
step="1"
|
||||
/>
|
||||
<p>Entered points: {{ points() ?? 'None' }}</p>
|
||||
`,
|
||||
})
|
||||
export class BookingComponent {
|
||||
points = signal<number | undefined>(undefined);
|
||||
export class MyComponent {
|
||||
pointsControl = new FormControl<number | null>(null);
|
||||
}
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
The directive implements three protection layers:
|
||||
|
||||
### 1. Keyboard Input Protection (`keydown`)
|
||||
Blocks invalid keys before they can be entered:
|
||||
- **Blocked:** `.`, `,`, `-`, `+`, `e`, `E`
|
||||
- **Allowed:** `0-9`, navigation keys (arrow keys, backspace, delete, tab)
|
||||
|
||||
### 2. Paste Protection (`paste`)
|
||||
Intercepts paste operations and sanitizes the content:
|
||||
- Extracts only digits from pasted text
|
||||
- Removes leading zeros
|
||||
- Updates the input value with the sanitized result
|
||||
|
||||
### 3. General Input Protection (`input`)
|
||||
Catches all other input methods (drag & drop, autofill, programmatic changes):
|
||||
- Validates and sanitizes any value changes
|
||||
- Ensures consistency across all input methods
|
||||
|
||||
## Examples
|
||||
|
||||
### ✅ What Works
|
||||
|
||||
| User Action | Input Attempt | Result in Field | Explanation |
|
||||
|-------------|---------------|-----------------|-------------|
|
||||
| **Typing** | `123` | `123` | Valid positive integer |
|
||||
| **Typing** | `1-2-3` | `123` | Minus signs blocked during typing |
|
||||
| **Typing** | `1.5` | `15` | Decimal point blocked, only digits entered |
|
||||
| **Paste** | `-100` | `100` | Negative sign removed |
|
||||
| **Paste** | `1.000` | `1000` | Decimal point removed |
|
||||
| **Paste** | `1,58` | `158` | Comma removed |
|
||||
| **Paste** | `+42` | `42` | Plus sign removed |
|
||||
| **Paste** | `3.14e2` | `314` | Scientific notation sanitized |
|
||||
| **Paste** | `00123` | `123` | Leading zeros removed |
|
||||
| **Paste** | `abc123xyz` | `123` | Non-digit characters removed |
|
||||
| **Typing** | `007` | `7` | Leading zeros removed |
|
||||
|
||||
### ❌ What Doesn't Work (By Design)
|
||||
|
||||
| User Action | Input Attempt | Result | Explanation |
|
||||
|-------------|---------------|--------|-------------|
|
||||
| **Typing/Paste** | `-50` | `50` | Negative numbers converted to positive |
|
||||
| **Typing/Paste** | `12.34` | `1234` | Decimals removed (not rounded) |
|
||||
| **Typing/Paste** | `0` | `` (empty) | Single zero removed (use `min="0"` if needed) |
|
||||
| **Paste** | `abc` | `` (empty) | No digits to extract |
|
||||
| **Paste** | `---` | `` (empty) | No digits to extract |
|
||||
|
||||
## Important Notes
|
||||
|
||||
### Zero Handling
|
||||
The directive removes leading zeros, which means a single `0` input will result in an empty field. If you need to allow zero as a valid value, consider:
|
||||
### With Reactive Forms (FormGroup)
|
||||
|
||||
```html
|
||||
<!-- Add min="0" and handle empty state in your component -->
|
||||
<input
|
||||
type="number"
|
||||
positiveIntegerInput
|
||||
[(ngModel)]="points"
|
||||
min="0"
|
||||
/>
|
||||
<form [formGroup]="orderForm">
|
||||
<input
|
||||
type="number"
|
||||
positiveIntegerInput
|
||||
formControlName="quantity"
|
||||
placeholder="Enter quantity"
|
||||
/>
|
||||
</form>
|
||||
```
|
||||
|
||||
### Decimal Numbers
|
||||
This directive is **not suitable** for decimal number inputs. Pasted decimals like `1.58` become `158`, not `1` or `2`. For decimal inputs, use a different validation approach.
|
||||
```ts
|
||||
import { FormBuilder, FormGroup } from '@angular/forms';
|
||||
|
||||
### Form Validation
|
||||
The directive sanitizes input but doesn't perform validation. Combine it with Angular form validators:
|
||||
export class MyComponent {
|
||||
orderForm: FormGroup;
|
||||
|
||||
```typescript
|
||||
import { Validators } from '@angular/forms';
|
||||
|
||||
// In your component
|
||||
pointsControl = new FormControl(null, [
|
||||
Validators.required,
|
||||
Validators.min(1),
|
||||
Validators.max(1000)
|
||||
]);
|
||||
constructor(private fb: FormBuilder) {
|
||||
this.orderForm = this.fb.group({
|
||||
quantity: [null]
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Complete Example with Validation
|
||||
|
||||
```html
|
||||
<form [formGroup]="productForm">
|
||||
<label for="quantity">Quantity:</label>
|
||||
<input
|
||||
id="quantity"
|
||||
type="number"
|
||||
positiveIntegerInput
|
||||
formControlName="quantity"
|
||||
placeholder="Enter quantity"
|
||||
[class.error]="quantity.invalid && quantity.touched"
|
||||
/>
|
||||
|
||||
@if (quantity.invalid && quantity.touched) {
|
||||
<span class="error-message">
|
||||
Quantity is required and must be a positive integer
|
||||
</span>
|
||||
}
|
||||
</form>
|
||||
```
|
||||
|
||||
```ts
|
||||
import { Component } from '@angular/core';
|
||||
import { FormBuilder, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms';
|
||||
import { PositiveIntegerInputDirective } from '@isa/utils/positive-integer-input';
|
||||
|
||||
@Component({
|
||||
selector: 'app-product-form',
|
||||
standalone: true,
|
||||
imports: [ReactiveFormsModule, PositiveIntegerInputDirective],
|
||||
templateUrl: './product-form.component.html',
|
||||
})
|
||||
export class ProductFormComponent {
|
||||
productForm: FormGroup;
|
||||
|
||||
constructor(private fb: FormBuilder) {
|
||||
this.productForm = this.fb.group({
|
||||
quantity: [null, [Validators.required, Validators.min(1)]]
|
||||
});
|
||||
}
|
||||
|
||||
get quantity() {
|
||||
return this.productForm.get('quantity')!;
|
||||
}
|
||||
|
||||
onSubmit() {
|
||||
if (this.productForm.valid) {
|
||||
console.log('Quantity:', this.quantity.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Behavior Examples
|
||||
|
||||
### Keyboard Input
|
||||
|
||||
| User Action | Result | Reason |
|
||||
|-------------|--------|--------|
|
||||
| Types `123` | `123` appears | Valid digits allowed |
|
||||
| Types `1-2-3` | Only `123` appears | Minus signs blocked |
|
||||
| Types `1.5` | Only `15` appears | Decimal point blocked |
|
||||
| Types `1,000` | Only `1000` appears | Comma blocked |
|
||||
| Types `+42` | Only `42` appears | Plus sign blocked |
|
||||
| Types `1e5` | Only `15` appears | Letter `e` blocked |
|
||||
|
||||
### Paste Operations
|
||||
|
||||
| Clipboard Content | Result | Reason |
|
||||
|------------------|--------|--------|
|
||||
| `-100` | `100` | Negative sign removed |
|
||||
| `1.000` | `1000` | Decimal point removed |
|
||||
| `1,58` | `158` | Comma removed (NOT rounded to `1` or `2`) |
|
||||
| `abc123xyz` | `123` | Non-digit characters removed |
|
||||
| `007` | `7` | Leading zeros removed |
|
||||
| `+42` | `42` | Plus sign removed |
|
||||
| `3.14e2` | `3142` | All non-digits removed |
|
||||
| `0` | Empty field | Single zero becomes empty string |
|
||||
|
||||
### Other Input Methods
|
||||
|
||||
The directive also sanitizes input from:
|
||||
- **Drag and drop** - Dropping text from other sources
|
||||
- **Browser autofill** - Autocomplete values
|
||||
- **Voice input** - Speech-to-text input
|
||||
- **IME input** - Input Method Editors (Asian languages)
|
||||
- **Right-click paste** - Context menu paste (some browsers)
|
||||
|
||||
## Browser Compatibility
|
||||
|
||||
The directive uses standard browser APIs and works in all modern browsers:
|
||||
- Chrome/Edge (Chromium-based)
|
||||
- Firefox
|
||||
- Safari
|
||||
- Mobile browsers (iOS Safari, Chrome Mobile)
|
||||
Works with all modern browsers that support Angular 20+ and native `input[type="number"]` elements.
|
||||
|
||||
## Running Unit Tests
|
||||
## Testing
|
||||
|
||||
Run `nx test utils-positive-integer-input` to execute the unit tests.
|
||||
The directive includes comprehensive test coverage with Vitest, including:
|
||||
- Keyboard input blocking tests
|
||||
- Paste sanitization tests
|
||||
- General input validation tests
|
||||
- Angular forms integration tests
|
||||
- Edge case handling
|
||||
|
||||
Run tests:
|
||||
```bash
|
||||
nx test utils-positive-integer-input
|
||||
```
|
||||
|
||||
## Related
|
||||
|
||||
- [Angular Forms Documentation](https://angular.dev/guide/forms)
|
||||
- [Angular Directives Documentation](https://angular.dev/guide/directives)
|
||||
- [HTML Input Element](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/number)
|
||||
|
||||
@@ -1,75 +1,301 @@
|
||||
# Scroll Position Library
|
||||
# @isa/utils/scroll-position
|
||||
|
||||
Utility library providing scroll position restoration and scroll-to-top functionality for Angular applications.
|
||||
|
||||
## Overview
|
||||
|
||||
The `@isa/utils/scroll-position` library provides a comprehensive set of utilities for managing scroll position persistence and viewport visibility detection across route navigation and component lifecycle changes. It enables seamless user experience by automatically preserving and restoring scroll positions when users navigate between views, particularly useful in list/detail navigation patterns.
|
||||
The `@isa/utils/scroll-position` library enables automatic scroll position preservation across route navigations and provides a reusable scroll-to-top button component. It stores scroll positions in session storage and restores them when users navigate back to previously visited routes, creating a smoother user experience for applications with long, scrollable content.
|
||||
|
||||
**Key Features:**
|
||||
- **Automatic Scroll Restoration**: Preserve and restore scroll position across route navigation
|
||||
- **Session Storage Integration**: Persists scroll positions in session storage for reliability
|
||||
- **Route-Level Control**: Enable/disable restoration per route via route data configuration
|
||||
- **Viewport Detection**: Observe when elements enter or leave the viewport for lazy loading and infinite scroll
|
||||
- **Scroll-to-Top Button**: Ready-to-use component with accessibility support
|
||||
- **Configurable Delays**: Control timing of scroll restoration with optional delays
|
||||
|
||||
**Type:** Utility library with functions, injectables, and directives
|
||||
**Type:** Utility library with functions, providers, and components
|
||||
|
||||
## Features
|
||||
|
||||
- Store current scroll position in session storage.
|
||||
- Restore saved scroll position with an optional delay.
|
||||
- Observe when an element enters or leaves the viewport.
|
||||
|
||||
## Usage
|
||||
|
||||
### Storing Scroll Position
|
||||
|
||||
Call the function to save the current scroll position:
|
||||
## Installation
|
||||
|
||||
```typescript
|
||||
import { storeScrollPosition } from '@isa/utils/scroll-position';
|
||||
|
||||
storeScrollPosition();
|
||||
import {
|
||||
provideScrollPositionRestoration,
|
||||
injectRestoreScrollPosition,
|
||||
storeScrollPosition,
|
||||
ScrollTopButtonComponent
|
||||
} from '@isa/utils/scroll-position';
|
||||
```
|
||||
|
||||
### Restoring Scroll Position
|
||||
## API Reference
|
||||
|
||||
Inject the restore function and call it with an optional delay:
|
||||
|
||||
```typescript
|
||||
import { injectRestoreScrollPosition } from '@isa/utils/scroll-position';
|
||||
|
||||
const restorePosition = injectRestoreScrollPosition();
|
||||
await restorePosition(200);
|
||||
```
|
||||
|
||||
### Automatic Restoration
|
||||
|
||||
Provide environment initializer in your app module to auto-save scroll positions:
|
||||
### Providers
|
||||
|
||||
#### `provideScrollPositionRestoration()`
|
||||
|
||||
Provides an environment initializer that automatically stores scroll positions during navigation and page unload events.
|
||||
|
||||
**Returns:** `EnvironmentProviders`
|
||||
|
||||
**Behavior:**
|
||||
- Listens to `NavigationStart` events from the Angular Router
|
||||
- Listens to the `beforeunload` window event
|
||||
- Only stores scroll position for routes with `scrollPositionRestoration: true` in their route data
|
||||
- Stores positions in session storage using the current URL as the key
|
||||
|
||||
**Usage:**
|
||||
|
||||
```typescript
|
||||
// In app.config.ts
|
||||
import { provideScrollPositionRestoration } from '@isa/utils/scroll-position';
|
||||
|
||||
provideScrollPositionRestoration();
|
||||
export const appConfig: ApplicationConfig = {
|
||||
providers: [
|
||||
provideScrollPositionRestoration(),
|
||||
// other providers...
|
||||
]
|
||||
};
|
||||
```
|
||||
|
||||
### Marking a Route for Auto Restoration
|
||||
### Functions
|
||||
|
||||
To enable automatic scroll restoration on a specific route, add the “scrollPositionRestoration” property in its data:
|
||||
#### `storeScrollPosition()`
|
||||
|
||||
Stores the current viewport scroll position in session storage using the current router URL as the key.
|
||||
|
||||
**Returns:** `Promise<void>`
|
||||
|
||||
**Usage:**
|
||||
|
||||
```typescript
|
||||
{
|
||||
path: 'example',
|
||||
component: ExampleComponent,
|
||||
data: {
|
||||
scrollPositionRestoration: true
|
||||
import { Component } from '@angular/core';
|
||||
import { storeScrollPosition } from '@isa/utils/scroll-position';
|
||||
|
||||
@Component({
|
||||
selector: 'app-product-list',
|
||||
// ...
|
||||
})
|
||||
export class ProductListComponent {
|
||||
async onNavigateAway() {
|
||||
// Manually store scroll position before navigating
|
||||
await storeScrollPosition();
|
||||
// navigation logic...
|
||||
}
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### Detecting Element Visibility
|
||||
#### `injectRestoreScrollPosition()`
|
||||
|
||||
Apply the directive to a component template to emit true or false when entering or exiting the viewport:
|
||||
Returns a function that restores the scroll position from session storage for the current route.
|
||||
|
||||
```html
|
||||
<div utilScrolledIntoViewport (utilScrolledIntoViewport)="onVisibilityChange($event)">...</div>
|
||||
**Returns:** `(delay?: number) => Promise<void>` - An async function that accepts an optional delay parameter
|
||||
|
||||
**Parameters (returned function):**
|
||||
- `delay?: number` - Optional delay in milliseconds before restoring scroll position (default: 0)
|
||||
|
||||
**Behavior:**
|
||||
- Retrieves stored scroll position using the current router URL as the key
|
||||
- Waits for the specified delay to ensure the DOM is ready
|
||||
- Clears the stored position after restoration
|
||||
- Does nothing if no position was stored for the current URL
|
||||
|
||||
**Usage:**
|
||||
|
||||
```typescript
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { injectRestoreScrollPosition } from '@isa/utils/scroll-position';
|
||||
|
||||
@Component({
|
||||
selector: 'app-product-list',
|
||||
// ...
|
||||
})
|
||||
export class ProductListComponent implements OnInit {
|
||||
private restoreScrollPosition = injectRestoreScrollPosition();
|
||||
|
||||
async ngOnInit() {
|
||||
// Restore scroll position after 100ms to ensure content is rendered
|
||||
await this.restoreScrollPosition(100);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Components
|
||||
|
||||
#### `ScrollTopButtonComponent`
|
||||
|
||||
A standalone component that displays a button to scroll back to the top of the page or a specific scrollable element.
|
||||
|
||||
**Selector:** `utils-scroll-top-button`
|
||||
|
||||
**Inputs:**
|
||||
- `target: Window | HTMLElement` - The scroll target (default: `window`)
|
||||
|
||||
**Features:**
|
||||
- Automatically shows/hides based on scroll position (shows when scrolled down)
|
||||
- Respects user's `prefers-reduced-motion` setting
|
||||
- Uses smooth scrolling when motion is not reduced
|
||||
- Includes proper accessibility attributes (`aria-label`)
|
||||
- E2E testing ready with `data-what` attribute
|
||||
|
||||
**Styling:**
|
||||
- Host class: `utils-scroll-top-button`
|
||||
- Uses `ViewEncapsulation.None` for flexible styling
|
||||
- Button uses tertiary color and large size from `@isa/ui/buttons`
|
||||
|
||||
**Usage:**
|
||||
|
||||
```typescript
|
||||
// Basic usage (scrolls window)
|
||||
import { ScrollTopButtonComponent } from '@isa/utils/scroll-position';
|
||||
|
||||
@Component({
|
||||
selector: 'app-product-list',
|
||||
imports: [ScrollTopButtonComponent],
|
||||
template: `
|
||||
<div class="content">
|
||||
<!-- Your scrollable content -->
|
||||
</div>
|
||||
<utils-scroll-top-button />
|
||||
`
|
||||
})
|
||||
export class ProductListComponent {}
|
||||
```
|
||||
|
||||
```typescript
|
||||
// Custom scroll target (specific element)
|
||||
@Component({
|
||||
selector: 'app-modal',
|
||||
imports: [ScrollTopButtonComponent],
|
||||
template: `
|
||||
<div #scrollContainer class="modal-content">
|
||||
<!-- Long content -->
|
||||
</div>
|
||||
<utils-scroll-top-button [target]="scrollContainer" />
|
||||
`
|
||||
})
|
||||
export class ModalComponent {
|
||||
@ViewChild('scrollContainer') scrollContainer!: ElementRef<HTMLElement>;
|
||||
}
|
||||
```
|
||||
|
||||
**Styling Example:**
|
||||
|
||||
```scss
|
||||
// Custom positioning and appearance
|
||||
utils-scroll-top-button {
|
||||
position: fixed;
|
||||
bottom: 2rem;
|
||||
right: 2rem;
|
||||
z-index: 1000;
|
||||
}
|
||||
```
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Complete Setup with Automatic Restoration
|
||||
|
||||
```typescript
|
||||
// 1. Configure in app.config.ts
|
||||
import { ApplicationConfig } from '@angular/core';
|
||||
import { provideScrollPositionRestoration } from '@isa/utils/scroll-position';
|
||||
|
||||
export const appConfig: ApplicationConfig = {
|
||||
providers: [
|
||||
provideScrollPositionRestoration(),
|
||||
]
|
||||
};
|
||||
|
||||
// 2. Enable in route configuration
|
||||
export const routes: Routes = [
|
||||
{
|
||||
path: 'products',
|
||||
component: ProductListComponent,
|
||||
data: { scrollPositionRestoration: true }
|
||||
}
|
||||
];
|
||||
|
||||
// 3. Restore position in component
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { injectRestoreScrollPosition, ScrollTopButtonComponent } from '@isa/utils/scroll-position';
|
||||
|
||||
@Component({
|
||||
selector: 'app-product-list',
|
||||
imports: [ScrollTopButtonComponent],
|
||||
template: `
|
||||
<div class="product-grid">
|
||||
@for (product of products(); track product.id) {
|
||||
<app-product-card [product]="product" />
|
||||
}
|
||||
</div>
|
||||
<utils-scroll-top-button />
|
||||
`
|
||||
})
|
||||
export class ProductListComponent implements OnInit {
|
||||
private restoreScrollPosition = injectRestoreScrollPosition();
|
||||
|
||||
async ngOnInit() {
|
||||
await this.loadProducts();
|
||||
// Restore scroll after content loads
|
||||
await this.restoreScrollPosition(100);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Manual Control for Custom Scenarios
|
||||
|
||||
```typescript
|
||||
import { Component } from '@angular/core';
|
||||
import { storeScrollPosition, injectRestoreScrollPosition } from '@isa/utils/scroll-position';
|
||||
|
||||
@Component({
|
||||
selector: 'app-custom-navigation',
|
||||
// ...
|
||||
})
|
||||
export class CustomNavigationComponent {
|
||||
private restoreScrollPosition = injectRestoreScrollPosition();
|
||||
|
||||
async onCustomNavigateAway() {
|
||||
// Store before custom navigation logic
|
||||
await storeScrollPosition();
|
||||
this.customNavigationService.navigate();
|
||||
}
|
||||
|
||||
async onCustomNavigateBack() {
|
||||
// Restore after returning
|
||||
await this.restoreScrollPosition(200);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Route Configuration
|
||||
|
||||
To enable automatic scroll restoration on specific routes, add the `scrollPositionRestoration` property in route data:
|
||||
|
||||
```typescript
|
||||
export const routes: Routes = [
|
||||
{
|
||||
path: 'example',
|
||||
component: ExampleComponent,
|
||||
data: {
|
||||
scrollPositionRestoration: true
|
||||
}
|
||||
}
|
||||
];
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
- `@angular/common` - ViewportScroller for scroll operations
|
||||
- `@angular/core` - Core Angular functionality (inject, signals, etc.)
|
||||
- `@angular/router` - Router integration for navigation events
|
||||
- `@isa/core/storage` - Session storage provider for persistence
|
||||
- `@isa/ui/buttons` - Icon button component for ScrollTopButtonComponent
|
||||
- `@isa/icons` - Icon assets (isaSortByUpMedium)
|
||||
- `@ng-icons/core` - Icon system integration
|
||||
|
||||
## Notes
|
||||
|
||||
- Scroll positions are stored in **session storage** and cleared after restoration
|
||||
- The automatic restoration only applies to routes with `scrollPositionRestoration: true` in their data
|
||||
- The scroll-to-top button respects accessibility preferences (`prefers-reduced-motion`)
|
||||
- Manual restoration includes a configurable delay to ensure DOM readiness before scrolling
|
||||
- The scroll-to-top button automatically shows/hides based on scroll position
|
||||
|
||||
Reference in New Issue
Block a user