npm package: @servicetitan/dte-pdf-editor
Description: A React component library for creating interactive PDF editors with drag-and-drop field placement, data model integration, e-signature support, fillable form fields, and calculated fields with formulas.
Latest version: 1.42.0

# @servicetitan/dte-pdf-editor

A React component library for creating interactive PDF editors with drag-and-drop field placement, data model
integration, e-signature support, fillable form fields, and calculated fields with formulas.

## Features

- 📄 **PDF Rendering**: Display and interact with PDF documents using `react-pdf` and `pdfjs-dist`
- 🎯 **Drag & Drop Field Placement**: Intuitively place fields on PDF pages by dragging from a sidebar
- 📊 **Data Model Integration**: Connect fields to structured data models using JSON Schema
- ✍️ **E-Signature Support**: Add signature, initials, date signed, and full name fields
- 📝 **Fillable Fields**: Support for text, number, date, checkbox, and radio button inputs
- 🧮 **Calculated Fields**: Formula-based fields with validation, autosuggest, and advanced formatting (number, currency,
  percent, date, rounding, separators)
- 📋 **Generic Fields**: Custom table and text blocks with configurable rows/cells and inline editing in view mode
- 👥 **Multi-Recipient Support**: Assign fields to different recipients with color-coded visualization
- ⚙️ **Field Configuration**: Configure field properties including position, size, label, recipient assignment,
  formula/format for calculated fields, and table structure (header/body rows and cells) for generic table fields
- 👁️ **View Mode**: Display PDFs with filled data in a read-only or interactive view mode (including generic fields via
  `PdfViewGeneric`)

### Recent changes (vs master) — feature/DTE-3900

- **Date fields in formulas**: Calculated fields now support date-typed fields from the data model and fillable date
  fields. Date arithmetic uses day-based semantics (date + number = add days, date - date = day difference). The `*` and
  `/` operators are disabled when date fields are present, enforced in both the UI and validation.
- **Date result type**: New `date` option in the result type selector. When the formula contains date fields, the result
  type is automatically set to `date`. Advanced settings show a customizable date format (tokens: `YYYY`, `YY`, `MMMM`,
  `MMM`, `MM`, `M`, `DD`, `D`) instead of numeric rounding/separator controls.
- **CalculatedFieldFormat**: Extended with `dateFormat?: string` (custom format pattern).
- **FormulaToken**: Field tokens now carry `fieldType?: 'date' | 'number'` to indicate whether a field is date- or
  number-typed, replacing the previously separate `dateFieldPaths` array on `CalculatedFieldFormat`.
- **SchemaFieldType**: New `fieldType` on `FieldTypeOption` tracks whether a schema field is `'number'` or `'date'`.
- **Validation**: Date formulas are restricted to `+`, `-`, and parentheses only. Numeric literals are capped at
  365 days.
- **Generic fields** (DTE-3811): Field type `generic` with subTypes `table` and `text`. Tables support configurable
  header and body rows with editable cells; text blocks support inline editing. Both are rendered in the overlay and in
  view mode.
- **PdfField**: Added optional `data?: FieldDataType` for generic fields (`GenericFieldTableDataType` for tables,
  `GenericFieldTextDataType` for text).
- **Field config panel**: **Table configs** (TableConfigs) for generic table fields—manage header visibility,
  header/body row heights, and cell values. Inline editable components for text and table content in the overlay.
- **Sidebar**: Generic field type list (Text, Table) in the field menu for drag-and-drop placement.
- **PdfView**: Generic fields are rendered in view mode via `PdfViewGeneric` with the same inline editing support.

## Installation

```bash
npm install @servicetitan/dte-pdf-editor
```

### Peer Dependencies

This package requires the following peer dependencies:

```bash
npm install @servicetitan/anvil2@^1.46.9 react@~18.3.1 react-dom@~18.3.1
```

## Quick Start

```tsx
import { PdfEditor, PdfField, PdfView, SchemaObject } from '@servicetitan/dte-pdf-editor';
import { useState } from 'react';

const pdfUrl = 'https://example.com/document.pdf';

function App() {
    const [fields, setFields] = useState<PdfField[]>([]);
    const [data, setData] = useState<Record<string, any>>({});

    const recipients = [
        { id: 1, name: 'technician', displayName: 'Technician' },
        { id: 2, name: 'customer', displayName: 'Customer' },
    ];

    const dataModel: SchemaObject = {
        type: 'object',
        properties: {
            CustomerName: {
                type: 'string',
                title: 'Customer Name',
                options: { placeholder: 'Enter customer name' },
            },
        },
    };

    return (
        <PdfEditor
            pdfUrl={pdfUrl}
            fields={fields}
            onFieldsChange={setFields}
            dataModel={dataModel}
            recipients={recipients}
        />
    );
}
```

## Components

### PdfEditor

The main component for editing PDF documents and placing fields.

#### Props

| Prop                 | Type                           | Required | Description                                             |
|----------------------|--------------------------------|----------|---------------------------------------------------------|
| `pdfUrl`             | `string`                       | Yes      | URL or path to the PDF file                             |
| `fields`             | `PdfField[]`                   | No       | Array of fields placed on the PDF                       |
| `onFieldsChange`     | `(fields: PdfField[]) => void` | Yes      | Callback when fields are added, modified, or deleted    |
| `dataModel`          | `SchemaObject`                 | No       | JSON Schema object defining available data model fields |
| `recipients`         | `RecipientInfo[]`              | No       | Array of recipients that can be assigned to fields      |
| `loading`            | `boolean`                      | No       | Whether the PDF is currently loading                    |
| `loadingPlaceholder` | `ReactNode`                    | No       | Custom component to display while PDF is loading        |
| `errorPlaceholder`   | `ReactNode`                    | No       | Custom component to display if PDF fails to load        |

#### Example

```tsx
<PdfEditor
    pdfUrl="https://example.com/document.pdf"
    fields={fields}
    onFieldsChange={setFields}
    dataModel={dataModel}
    recipients={recipients}
    loading={!pdfUrl}
    loadingPlaceholder={<Spinner />}
    errorPlaceholder={<Alert status="danger" title="Failed to load PDF" />}
/>
```

### PdfView

Component for displaying PDFs with filled data in view mode.

#### Props

| Prop                 | Type                                                           | Required | Description                                              |
|----------------------|----------------------------------------------------------------|----------|----------------------------------------------------------|
| `pdfUrl`             | `string`                                                       | Yes      | URL or path to the PDF file                              |
| `fields`             | `PdfField[]`                                                   | Yes      | Array of fields to display on the PDF                    |
| `data`               | `DataModelValues`                                              | No       | Data object containing values for data model fields      |
| `recipients`         | `RecipientInfo[]`                                              | No       | Array of recipients for field assignment                 |
| `fillingBy`          | `string[]`                                                     | No       | Array of recipient names that are allowed to fill fields |
| `onDataChange`       | `(changedData: { [path: string]: string \| boolean }) => void` | No       | Callback when fillable field data changes                |
| `loading`            | `boolean`                                                      | No       | Whether the PDF is currently loading                     |
| `loadingPlaceholder` | `ReactNode`                                                    | No       | Custom component to display while PDF is loading         |
| `errorPlaceholder`   | `ReactNode`                                                    | No       | Custom component to display if PDF fails to load         |

#### Example

```tsx
<PdfView
    pdfUrl="https://example.com/document.pdf"
    fields={fields}
    data={data}
    recipients={recipients}
    fillingBy={['technician']}
    onDataChange={(changedData) => {
        setData({ ...data, ...changedData });
    }}
/>
```

## Types

### PdfField

Represents a field placed on a PDF document.

```tsx
interface PdfField {
    id: string;
    type: FieldTypeEnum;
    subType?: PdfFieldSubType;
    x: number;
    y: number;
    page: number;
    label: string;
    width: number;
    height: number;
    required?: boolean;
    path?: string;
    recipient?: string;
    description?: string;
    data?: FieldDataType;                  // for generic fields (table/text)
    formula?: StructuredFormula;            // for calculated fields
    formulaFormat?: CalculatedFieldFormat;  // for calculated fields (includes dateFormat)
}
```

### FieldTypeEnum

Enumeration of field types:

- `dataModel`: Field connected to a data model schema
- `eSign`: E-signature field
- `fillable`: User-fillable form field
- `calculated`: Formula-based calculated field
- `generic`: Custom table or text block (configurable rows/cells, inline editing)

### ESignFieldType

Types of e-signature fields:

- `signature`: Signature field
- `initials`: Initials field
- `dateSigned`: Date signed field
- `fullName`: Full name field

### PdfFieldSubType

Union of fillable, e-sign, and generic sub-types: `FillableFieldType | ESignFieldType | GenericFieldType`. Used for
`PdfField.subType` and `FieldTypeOption.subType`.

### FillableFieldType

Types of fillable fields:

- `text`: Text input
- `number`: Number input
- `date`: Date picker
- `checkbox`: Checkbox
- `radio`: Radio button

### GenericFieldType

Types of generic fields:

- `table`: Configurable table with header and body rows and editable cells
- `text`: Text block with inline editing

### FieldDataType

For generic fields, `PdfField.data` can be:

- **GenericFieldTableDataType**: `showHeader`, `header` (height, cells), `body` (array of rows with height and cells)
- **GenericFieldTextDataType**: `value` (string)

### Calculated Fields (formula & format)

Calculated fields use a structured formula and optional display format:

- **StructuredFormula**: Token list (numbers, operators `+ - * /`, parentheses, field references) used for validation
  and safe editing.
- **CalculatedFieldFormat**: Controls result display: `resultType` (`number` | `currency` | `percent` | `date`),
  rounding mode, decimal places, thousands/decimal separators, prefix/postfix text, `dateFormat` (custom pattern using
  `YYYY`, `MM`, `DD`, etc.).
- **FormulaToken (field)**: Field tokens carry `fieldType?: 'date' | 'number'` to support date-aware evaluation
  directly from the formula structure.

Date fields in formulas use day-based arithmetic: `date + number` adds days, `date - date` yields a day count. Only `+`,
`-`, and parentheses are allowed when date fields are present.

Schema fields can opt in via `options.useInCalculatedFields` and `options.useInConditionals` for data model extraction
in formula builder and conditionals.

### SchemaObject

JSON Schema object defining the data model structure. Supports nested objects, arrays, and various string subtypes (
string, text, html, image).

```tsx
interface SchemaObject {
    type: 'object';
    properties: Record<string, SchemaNode>;
    title?: string;
    options?: SchemaFieldBaseOptions;
}
```

### RecipientInfo

Information about a recipient that can be assigned to fields.

```tsx
interface RecipientInfo {
    id: number;
    name: string;
    displayName: string;
}
```

## Usage Examples

### Basic Editor with Data Model

```tsx
import { PdfEditor, PdfField, SchemaObject } from '@servicetitan/dte-pdf-editor';
import { useState } from 'react';

const dataModel: SchemaObject = {
    type: 'object',
    properties: {
        JobInformation: {
            type: 'object',
            properties: {
                JobNumber: {
                    type: 'string',
                    title: 'Job Number',
                    options: { placeholder: 'Enter job number' },
                },
                JobName: {
                    type: 'string',
                    title: 'Job Name',
                    options: { placeholder: 'Enter job name' },
                },
            },
        },
    },
};

function MyEditor() {
    const [fields, setFields] = useState<PdfField[]>([]);

    return (
        <PdfEditor
            pdfUrl="https://example.com/document.pdf"
            fields={fields}
            onFieldsChange={setFields}
            dataModel={dataModel}
        />
    );
}
```

### Editor with Recipients

```tsx
const recipients = [
    { id: 1, name: 'technician', displayName: 'Technician' },
    { id: 2, name: 'customer', displayName: 'Customer' },
    { id: 3, name: 'accountant', displayName: 'Accountant' },
];

<PdfEditor
    pdfUrl={pdfUrl}
    fields={fields}
    onFieldsChange={setFields}
    recipients={recipients}
/>
```

### View Mode with Data

```tsx
const [data, setData] = useState({
    'JobInformation.JobNumber': 'JOB-12345',
    'JobInformation.JobName': 'HVAC Installation',
});

<PdfView
    pdfUrl={pdfUrl}
    fields={fields}
    data={data}
    recipients={recipients}
    fillingBy={['technician']}
    onDataChange={(changedData) => {
        setData({ ...data, ...changedData });
    }}
/>
```

### Toggle Between Edit and View Modes

```tsx
function PdfEditorPage() {
    const [fields, setFields] = useState<PdfField[]>([]);
    const [isView, setIsView] = useState(false);
    const [data, setData] = useState<Record<string, any>>({});

    return (
        <>
            <Button onClick={() => setIsView(!isView)}>
                {isView ? 'Edit' : 'View'}
            </Button>
            {!isView ? (
                <PdfEditor
                    pdfUrl={pdfUrl}
                    fields={fields}
                    onFieldsChange={setFields}
                    dataModel={dataModel}
                    recipients={recipients}
                />
            ) : (
                <PdfView
                    pdfUrl={pdfUrl}
                    fields={fields}
                    data={data}
                    recipients={recipients}
                    fillingBy={['technician']}
                    onDataChange={(changedData) => {
                        setData({ ...data, ...changedData });
                    }}
                />
            )}
        </>
    );
}
```

## Field Operations

### Adding Fields

Fields are added by dragging from the sidebar onto the PDF canvas. The sidebar displays:

- **Data Model Fields (Merge Tags)**: Grouped by schema structure
- **E-Sign Fields**: Signature, initials, date signed, full name
- **Fillable Fields**: Text, number, date, checkbox, radio
- **Calculated Fields**: Formula-based fields (configure formula and format in the config panel)
- **Generic Fields**: Text and Table (configure table rows/cells in the config panel; inline editing in view mode)

### Configuring Fields

Click on a field to open the configuration panel where you can:

- Change the label
- Adjust position and size
- Assign a recipient
- Set required status (fillable fields, except checkbox)
- Update the data model path
- For **calculated fields**: edit formula (with validation and autosuggest from data model + fillable fields), set
  result type (number/currency/percent/date), rounding, decimals, separators, and date format
- For **generic table fields**: configure header visibility, header/body row heights, and cell values via Table configs;
  generic text fields support inline editing in the overlay

### Moving and Resizing

- **Move**: Click and drag a field to reposition it
- **Resize**: Use the resize handles on field corners

### Deleting Fields

Select a field and use the delete button in the configuration panel, or press the delete key.

## Styling

The package includes default styles. To customize, you can override CSS classes:

- `.dte-pdf-editor`: Main editor container
- `.dte-pdf-view-container`: View mode container
- `.dte-pdf-wrapper`: PDF document wrapper
- `.dte-pdf-field-overlay`: Field overlay container
- `.dte-pdf-editor-sidebar-container`: Sidebar container

## Browser Support

This package requires modern browsers with support for:

- ES6+
- Canvas API
- Drag and Drop API

## License

Copyright © ServiceTitan