Skip to content

PDF Templates - Visual Document Designer

Design custom PDF layouts for reports, prescriptions, and consent forms without code.

What this enables

Branded documents: Create professional PDFs with your clinic's logo, colors, and branding—not generic templates.

Form → PDF conversion: Auto-generate consultation reports from form data—patient answers flow directly into your letterhead.

Reusable components: Build once, use everywhere—letterhead, signature blocks, disclaimers that appear on all documents.

Non-technical design: Drag blocks, insert form fields, preview in real-time—no HTML/CSS required (but available if needed).

Multi-document types: Different layouts for reports, prescriptions, invoices, consent forms—all with shared branding.

How it works

  1. Admin opens template designer: Creates "Consultation Report" template
  2. Drag blocks: Letterhead, patient name block, findings section, signature line
  3. Insert form fields: "Add patient city" → drag in form field → preview shows real data
  4. Preview: See exactly how the PDF will look (live refresh as you edit)
  5. Generate PDF: When form is completed, system auto-generates beautiful PDF with form answers
  6. Download/email: Patient gets PDF report emailed automatically

Technical Reference

Overview

PDF Templates enable organizations to create custom visual layouts for their documents (reports, prescriptions, disclaimers). This feature provides a minimal block-based editor for designing professional PDFs without writing HTML/CSS, while still allowing advanced users to customize the underlying template code.

Core Concept

PDF templates are visual layouts that transform form data into branded, professional documents. They bridge the gap between:

  • Forms (structured data collection)
  • Documents (PDF outputs for patients/specialists)
v-pre
Form Data (JSONB)  +  PDF Template (HTML/CSS)  =  Professional PDF
   {"field_10": "Amsterdam"}  +  {{.FormValues.field_10}}  =  City: Amsterdam

Why This Design?

ProblemSolution
"Every org needs different PDF layouts"Per-organization template customization
"Non-technical users can't write HTML"Block-based visual editor with drag-drop
"Need to insert form field values dynamically"Go template syntax with autocomplete
"Header/footer must be consistent"Reusable components (letterhead, signature blocks)
"Multiple document types share layout"Template inheritance (base template + overrides)
"Preview must match final PDF"Live preview using same chromedp renderer

Architecture Overview

┌─────────────────────────────────────────────────────────┐
│  PDF Template Feature                                    │
└─────────────────────────────────────────────────────────┘

1. Template Designer (Admin UI)
   ├─ Block-based editor (drag sections, text, tables, images)
   ├─ Field picker (insert form field placeholders)
   ├─ Component library (letterhead, signature, disclaimers)
   ├─ Live preview (real-time render with sample data)
   └─ HTML/CSS code view (for advanced customization)

2. Template Storage (Backend)
   ├─ pdf_templates table (HTML, CSS, metadata)
   ├─ pdf_template_versions (versioning like custom fields)
   ├─ pdf_template_components (reusable blocks)
   └─ Organization-scoped (multi-tenant)

3. Rendering Engine (chromedp)
   ├─ Merge template + form data → HTML
   ├─ Apply CSS styling
   ├─ Convert HTML → PDF (headless Chrome)
   └─ Cache signed documents to S3

Template Types

TypeDocument TypeUse Case
reportConsultation ReportPatient visit summaries, specialist observations
prescriptionPrescriptionMedication orders, treatment plans
disclaimerConsent/LegalGDPR consent, HIPAA notice, procedure disclaimers
certificateCertificatesCompletion certificates, medical clearance
invoiceBillingInvoice for services rendered

Template Structure

Database Schema

sql
CREATE TABLE pdf_templates (
    id                  BIGSERIAL PRIMARY KEY,
    organization_id     BIGINT NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,

    -- Metadata
    name                TEXT NOT NULL,
    description         TEXT,
    template_type       TEXT NOT NULL,  -- 'report', 'prescription', 'disclaimer', 'certificate', 'invoice'

    -- Template content
    template_html       TEXT NOT NULL,  -- HTML template with Go template syntax
    template_css        TEXT,           -- Custom CSS styling

    -- Layout config (JSONB)
    layout_config       JSONB NOT NULL DEFAULT '{}',
    -- {
    --   "pageSize": "A4",
    --   "orientation": "portrait",
    --   "margins": {"top": 20, "right": 20, "bottom": 20, "left": 20},
    --   "header": true,
    --   "footer": true
    -- }

    -- Versioning
    version             INT NOT NULL DEFAULT 1,
    published           BOOLEAN NOT NULL DEFAULT FALSE,

    -- Component references (reusable blocks)
    components_used     JSONB,  -- ["letterhead", "signature_block", "footer"]

    created_at          TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at          TIMESTAMPTZ NOT NULL DEFAULT NOW(),

    UNIQUE (organization_id, name)
);

CREATE TABLE pdf_template_versions (
    id                  BIGSERIAL PRIMARY KEY,
    template_id         BIGINT NOT NULL REFERENCES pdf_templates(id) ON DELETE CASCADE,
    organization_id     BIGINT NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
    version             INT NOT NULL,
    template_html       TEXT NOT NULL,
    template_css        TEXT,
    layout_config       JSONB NOT NULL,
    changed_by          BIGINT REFERENCES users(id),
    created_at          TIMESTAMPTZ NOT NULL DEFAULT NOW(),

    UNIQUE (template_id, version)
);

CREATE TABLE pdf_template_components (
    id                  BIGSERIAL PRIMARY KEY,
    organization_id     BIGINT NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,

    name                TEXT NOT NULL,
    description         TEXT,
    component_html      TEXT NOT NULL,
    component_css       TEXT,

    -- Component category
    category            TEXT NOT NULL,  -- 'header', 'footer', 'signature', 'letterhead', 'table', 'section'

    created_at          TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at          TIMESTAMPTZ NOT NULL DEFAULT NOW(),

    UNIQUE (organization_id, name)
);

Template Variables (Go Template Syntax)

Templates use Go's text/template package for dynamic content:

Available Variables

go
type TemplateData struct {
    // Document metadata
    DocumentID      int64
    DocumentType    string
    GeneratedAt     time.Time

    // Organization
    Organization    Organization
    // {
    //   "name": "RestartiX Clinic Bucharest",
    //   "address": "Strada Victoriei 123",
    //   "phone": "+40 21 123 4567",
    //   "logo_url": "https://s3.../logo.png"
    // }

    // Patient
    Patient         Patient
    // {
    //   "name": "Ion Popescu",
    //   "email": "[email protected]",
    //   "phone": "+40 700 123 456",
    //   "birthdate": "1985-05-15"
    // }

    // Specialist
    Specialist      Specialist
    // {
    //   "name": "Dr. Maria Ionescu",
    //   "title": "MD, Physiotherapist",
    //   "license_number": "12345",
    //   "signature_url": "https://s3.../signature.png"
    // }

    // Appointment
    Appointment     Appointment
    // {
    //   "started_at": "2025-02-15T10:00:00Z",
    //   "ended_at": "2025-02-15T11:00:00Z",
    //   "service_name": "Spine Consultation"
    // }

    // Form data (from signed form)
    FormValues      map[string]interface{}
    // {
    //   "field_10": "Amsterdam",
    //   "field_11": "Big pain",
    //   "field_15": "2025-02-15"
    // }

    FormFields      []FormField
    // [
    //   {"custom_field_id": 10, "label": "City", "value": "Amsterdam"},
    //   {"custom_field_id": 11, "label": "Pain Level", "value": "Big pain"}
    // ]
}

Template Syntax Examples

html
<!-- Organization letterhead -->
<div class="letterhead">
  <img src="{{.Organization.LogoURL}}" alt="Logo" />
  <h1>{{.Organization.Name}}</h1>
  <p>{{.Organization.Address}} | {{.Organization.Phone}}</p>
</div>

<!-- Patient info -->
<div class="patient-info">
  <h2>Patient Information</h2>
  <p><strong>Name:</strong> {{.Patient.Name}}</p>
  <p><strong>Date of Birth:</strong> {{.Patient.Birthdate}}</p>
</div>

<!-- Form fields (dynamic loop) -->
<div class="form-data">
  <h2>Consultation Details</h2>
  {{range .FormFields}}
    <div class="field">
      <strong>{{.Label}}:</strong> {{.Value}}
    </div>
  {{end}}
</div>

<!-- Specific field by ID -->
<p>Chief Complaint: {{index .FormValues "field_10"}}</p>

<!-- Conditional rendering -->
{{if .Specialist.SignatureURL}}
  <div class="signature">
    <img src="{{.Specialist.SignatureURL}}" alt="Signature" />
    <p>{{.Specialist.Name}}, {{.Specialist.Title}}</p>
  </div>
{{end}}

<!-- Date formatting -->
<p>Generated on: {{.GeneratedAt.Format "January 2, 2006"}}</p>

Minimal Block-Based Editor

Instead of external editors, build a simple custom editor with these core blocks:

Block Types

typescript
// Block definitions
type Block = {
  id: string;
  type: BlockType;
  config: BlockConfig;
  children?: Block[];
};

enum BlockType {
  // Layout blocks
  SECTION = 'section',           // Container for other blocks
  COLUMNS = 'columns',           // 2-3 column layout

  // Content blocks
  TEXT = 'text',                 // Rich text (bold, italic, underline)
  HEADING = 'heading',           // H1-H4
  PARAGRAPH = 'paragraph',       // Body text
  IMAGE = 'image',               // Logo, signature, uploaded images
  TABLE = 'table',               // Data tables

  // Dynamic blocks
  FIELD = 'field',               // Single form field value
  FIELD_LIST = 'field_list',     // All form fields as list
  FIELD_TABLE = 'field_table',   // All form fields as table

  // Components
  LETTERHEAD = 'letterhead',     // Org header
  SIGNATURE = 'signature',       // Specialist signature
  FOOTER = 'footer',             // Page footer
}

// Example block config
interface TextBlockConfig {
  content: string;
  fontSize: number;
  fontWeight: 'normal' | 'bold';
  textAlign: 'left' | 'center' | 'right';
  color: string;
}

interface FieldBlockConfig {
  fieldId: number;              // custom_field_id
  label: string;
  showLabel: boolean;
  format?: 'text' | 'date' | 'currency';
}

Editor UI Structure

┌──────────────────────────────────────────────────────────┐
│  PDF Template Editor                                      │
├──────────────────────────────────────────────────────────┤
│                                                           │
│  [Block Palette]     [Canvas]            [Preview]       │
│                                                           │
│  📝 Text             ┌─────────────┐    ┌──────────┐    │
│  📋 Heading          │ LETTERHEAD  │    │ PDF      │    │
│  📊 Table            ├─────────────┤    │ Preview  │    │
│  🖼️  Image           │ Patient Info│    │          │    │
│  📄 Field            ├─────────────┤    │ [Live]   │    │
│  📑 Field List       │ Form Fields │    │          │    │
│  🏢 Letterhead       ├─────────────┤    │          │    │
│  ✍️  Signature        │ SIGNATURE   │    │          │    │
│  📌 Footer           └─────────────┘    └──────────┘    │
│                                                           │
│  [Properties Panel]                                       │
│  ┌────────────────────────────────────┐                  │
│  │ Selected: Heading Block            │                  │
│  │ • Font Size: [24px]                │                  │
│  │ • Font Weight: [Bold ▼]            │                  │
│  │ • Alignment: [Left ▼]              │                  │
│  │ • Color: [#000000]                 │                  │
│  └────────────────────────────────────┘                  │
│                                                           │
│  [< Back]  [Save Draft]  [Preview]  [Publish Template]  │
└──────────────────────────────────────────────────────────┘

Editor Features (Minimal)

  1. Drag & Drop Blocks - Add blocks from palette to canvas
  2. Inline Editing - Click text to edit content
  3. Property Panel - Configure selected block (font size, color, alignment)
  4. Field Picker - Select form fields to insert (autocomplete)
  5. Live Preview - Render PDF in iframe using sample data
  6. Component Library - Save/reuse common blocks (letterhead, footer)
  7. HTML/CSS View - Switch to code editor for advanced users
  8. Sample Data - Test template with mock form values

How It Works: End-to-End Flow

┌─────────────────────────────────────────────────────────┐
│  1. TEMPLATE CREATION (Admin)                            │
└─────────────────────────────────────────────────────────┘

1. Admin navigates to PDF Templates
2. Clicks "Create New Template"
3. Selects type: "Report"
4. Uses block editor to design layout:
   ├─ Add Letterhead component
   ├─ Add Heading: "Consultation Report"
   ├─ Add Patient Info section
   ├─ Add Field List (all form fields)
   ├─ Add Signature component
   └─ Add Footer component
5. Clicks "Preview" → Sees PDF with sample data
6. Adjusts styling in properties panel
7. Clicks "Publish Template" → Version 1 created

┌─────────────────────────────────────────────────────────┐
│  2. PDF GENERATION (Runtime)                             │
└─────────────────────────────────────────────────────────┘

1. Specialist fills and signs form
2. Form status: completed → signed
3. Document service:
   ├─ Reads pdf_templates WHERE type = 'report'
   ├─ Reads form.values and form.fields
   ├─ Merges template + data using Go templates
   ├─ Applies CSS styling
   ├─ Renders HTML → PDF with chromedp
   └─ Caches PDF to S3
4. Patient downloads PDF

┌─────────────────────────────────────────────────────────┐
│  3. TEMPLATE UPDATES (Versioning)                        │
└─────────────────────────────────────────────────────────┘

1. Admin edits template
2. Changes are saved as draft (published = false)
3. Admin clicks "Publish"
   ├─ Current version archived to pdf_template_versions
   ├─ version incremented (1 → 2)
   ├─ published = true
4. New documents use version 2
5. Old documents still reference version 1 (immutable)

Integration Points

With Forms Feature

sql
-- Forms store which PDF template to use
ALTER TABLE form_templates ADD COLUMN pdf_template_id BIGINT
  REFERENCES pdf_templates(id);

-- Example: Intake survey form → uses "Report" PDF template
UPDATE form_templates
SET pdf_template_id = (SELECT id FROM pdf_templates WHERE name = 'Standard Report')
WHERE type = 'survey';

With Documents Feature

sql
-- Documents reference the PDF template version used
ALTER TABLE appointment_documents ADD COLUMN pdf_template_version INT;

-- When generating PDF, store which version was used
UPDATE appointment_documents
SET pdf_template_version = 2
WHERE id = 123;

API Endpoints

See api.md for complete API documentation.

Quick reference:

POST   /v1/pdf-templates              Create template
GET    /v1/pdf-templates              List templates
GET    /v1/pdf-templates/{id}         Get template
PUT    /v1/pdf-templates/{id}         Update template (draft)
POST   /v1/pdf-templates/{id}/publish Publish new version
DELETE /v1/pdf-templates/{id}         Delete template

GET    /v1/pdf-templates/{id}/preview Preview with sample data
GET    /v1/pdf-templates/{id}/render  Render with real form data

POST   /v1/pdf-template-components    Create reusable component
GET    /v1/pdf-template-components    List components

Sample Template (HTML Output)

html
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <style>
    @page {
      size: A4;
      margin: 20mm;
    }
    body {
      font-family: 'Helvetica', 'Arial', sans-serif;
      font-size: 12pt;
      line-height: 1.6;
      color: #333;
    }
    .letterhead {
      text-align: center;
      border-bottom: 2px solid #003366;
      padding-bottom: 10mm;
      margin-bottom: 10mm;
    }
    .letterhead img {
      max-width: 150px;
      margin-bottom: 5mm;
    }
    .section {
      margin-bottom: 10mm;
    }
    .field {
      margin-bottom: 3mm;
    }
    .field strong {
      min-width: 150px;
      display: inline-block;
    }
    .signature {
      margin-top: 20mm;
      text-align: right;
    }
    .signature img {
      max-width: 200px;
    }
    .footer {
      position: fixed;
      bottom: 0;
      left: 0;
      right: 0;
      text-align: center;
      font-size: 10pt;
      color: #666;
      border-top: 1px solid #ccc;
      padding-top: 3mm;
    }
  </style>
</head>
<body>
  <!-- Letterhead -->
  <div class="letterhead">
    <img src="{{.Organization.LogoURL}}" alt="{{.Organization.Name}}" />
    <h1>{{.Organization.Name}}</h1>
    <p>{{.Organization.Address}} | {{.Organization.Phone}}</p>
  </div>

  <!-- Document Title -->
  <h2 style="text-align: center; color: #003366;">Consultation Report</h2>

  <!-- Patient Information -->
  <div class="section">
    <h3>Patient Information</h3>
    <div class="field"><strong>Name:</strong> {{.Patient.Name}}</div>
    <div class="field"><strong>Date of Birth:</strong> {{.Patient.Birthdate}}</div>
    <div class="field"><strong>Date:</strong> {{.Appointment.StartedAt.Format "January 2, 2006"}}</div>
  </div>

  <!-- Consultation Details -->
  <div class="section">
    <h3>Consultation Details</h3>
    {{range .FormFields}}
      {{if not .IsPrivate}}
        <div class="field">
          <strong>{{.Label}}:</strong> {{.Value}}
        </div>
      {{end}}
    {{end}}
  </div>

  <!-- Specialist Signature -->
  <div class="signature">
    {{if .Specialist.SignatureURL}}
      <img src="{{.Specialist.SignatureURL}}" alt="Signature" />
    {{end}}
    <p><strong>{{.Specialist.Name}}</strong><br>
       {{.Specialist.Title}}<br>
       License: {{.Specialist.LicenseNumber}}</p>
  </div>

  <!-- Footer -->
  <div class="footer">
    <p>{{.Organization.Name}} | Confidential Medical Document | Generated: {{.GeneratedAt.Format "2006-01-02 15:04"}}</p>
  </div>
</body>
</html>

Benefits Summary

No external dependencies - Custom editor built in-house ✅ Organization branding - Each org customizes their layouts ✅ Non-technical users - Drag-drop blocks, no HTML needed ✅ Advanced users - Can edit HTML/CSS directly ✅ Consistent output - Same renderer for preview and final PDF ✅ Versioning - Old documents remain unchanged when templates update ✅ Reusable components - Letterhead, signature blocks shared across templates ✅ Multi-document types - One feature handles reports, prescriptions, disclaimers, etc.


Next Steps

  1. Read schema.md for complete database design
  2. Read api.md for API endpoint specifications
  3. Read editor.md for visual editor implementation guide
  4. Read rendering.md for chromedp rendering details
  5. Read components.md for reusable component library