Skip to content

Hamad-Center/invoice-craft

Repository files navigation

🧾 Invoice Craft

Customizable invoice PDF generator for React, Vue, and vanilla JS - TypeScript, browser-first

npm version TypeScript License: MIT

A modern, lightweight TypeScript library for generating professional invoice PDFs directly in the browser. Perfect for web applications that need white-label invoice generation without server dependencies.

✨ Features

Core Features

  • 🎨 Fully Customizable - Brand colors, logos, custom fields, and layouts
  • 🌐 Framework Agnostic - Works with React, Vue, Angular, or vanilla JavaScript
  • πŸ“± Browser-First - No server required, generates PDFs client-side
  • πŸ”§ TypeScript Ready - Full type safety and IntelliSense support
  • 🌍 Internationalization - Multi-language support with custom labels
  • βœ… Advanced Validation - Detailed validation with errors and warnings
  • πŸ“„ Multiple Templates - Professional templates ready to use
  • 🎯 Lightweight - Minimal bundle impact with tree-shaking support

Advanced Features

  • πŸ” Live HTML Preview - Preview invoices before PDF generation
  • πŸ“€ Multiple Export Formats - PDF, HTML, JSON, and CSV export
  • ⚑ Batch Processing - Generate multiple invoices efficiently
  • πŸ”Œ Plugin System - Extensible architecture for custom functionality
  • 🎨 Custom Templates - Build your own templates with the template builder
  • πŸ“Š Progress Tracking - Real-time progress for batch operations
  • πŸ›‘οΈ Schema Validation - Comprehensive data validation with detailed feedback

πŸš€ Quick Start

Installation

Package Manager Command
npm npm install invoice-craft
yarn yarn add invoice-craft
pnpm pnpm add invoice-craft
bun bun add invoice-craft

Basic Usage

import { generateInvoicePdf } from 'invoice-craft';

const invoiceData = {
  from: { name: "Your Company", address: "123 Business St", email: "contact@company.com" },
  to: { name: "Client Name", address: "456 Client Ave", email: "client@email.com" },
  invoiceNumber: "INV-001",
  invoiceDate: "2024-01-15",
  items: [{ description: "Web Development", quantity: 40, unitPrice: 125.00, taxRate: 0.10 }],
  currency: "USD"
};

const { blob, filename } = await generateInvoicePdf(invoiceData);
// Download logic here...

Module Support

Type Import Statement
ES Modules import { generateInvoicePdf } from 'invoice-craft'
CommonJS const { generateInvoicePdf } = require('invoice-craft')
Dynamic const { generateInvoicePdf } = await import('invoice-craft')

🎨 Customization Options

Brand Customization

Option Type Description Example
brandColor string Primary brand color "#3b82f6"
logoUrl string Company logo URL "https://logo.png"
layoutStyle string Template style "modern", "minimal", "creative"
filenameTemplate function Custom filename ({ invoice }) => "inv-${invoice.invoiceNumber}.pdf"

Internationalization Support

Language Code Status
English en βœ… Default
Spanish es βœ… Available
French fr βœ… Available
German de βœ… Available
Custom - βœ… Configurable
const spanishLabels = {
  invoice: "Factura", invoiceNumber: "NΓΊmero de Factura",
  date: "Fecha", total: "Total" /* ... more labels */
};
await generateInvoicePdf(invoiceData, { labels: spanishLabels });

Validation Options

Function Purpose Returns
validateInvoice() Basic validation ValidationResult
validateInvoiceStrict() Strict validation ValidationResult
Custom validation Business rules User-defined
import { validateInvoice } from 'invoice-craft';
const result = validateInvoice(invoiceData);
if (!result.isValid) console.log(result.errors);

πŸ–₯️ Framework Integration

Framework Support

Framework Status Features Example
React βœ… Full Support Hooks, TypeScript, Components View Example
Vue 3 βœ… Full Support Composition API, TypeScript View Example
Angular βœ… Compatible Services, TypeScript View Example
Svelte βœ… Compatible Stores, TypeScript View Example
Node.js βœ… Server-side Express, API endpoints View Example

React Example

import { generateInvoicePdf, validateInvoice } from 'invoice-craft';

function InvoiceGenerator({ invoiceData }) {
  const [isGenerating, setIsGenerating] = useState(false);

  const handleGenerate = async () => {
    setIsGenerating(true);
    try {
      const validation = validateInvoice(invoiceData);
      if (!validation.isValid) throw new Error('Invalid data');

      const { blob, filename } = await generateInvoicePdf(invoiceData);
      // Download logic...
    } catch (error) {
      console.error(error);
    } finally {
      setIsGenerating(false);
    }
  };

  return (
    <button onClick={handleGenerate} disabled={isGenerating}>
      {isGenerating ? 'Generating...' : 'Generate PDF'}
    </button>
  );
}

Vue Example

<template>
  <div>
    <button @click="generatePDF" :disabled="isGenerating">
      {{ isGenerating ? 'Generating...' : 'Generate PDF' }}
    </button>
    <div v-if="validation && !validation.isValid">
      <p v-for="error in validation.errors" :key="error.field">
        {{ error.field }}: {{ error.message }}
      </p>
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue';
import { generateInvoicePdf, validateInvoice } from 'invoice-craft';

const isGenerating = ref(false);
const validation = ref(null);

const generatePDF = async () => {
  isGenerating.value = true;
  try {
    validation.value = validateInvoice(invoiceData);
    if (!validation.value.isValid) return;

    const { blob, filename } = await generateInvoicePdf(invoiceData);
    // Download logic...
  } finally {
    isGenerating.value = false;
  }
};
</script>

Modern Vanilla JavaScript

import {
  generateInvoicePdf,
  generatePreviewHTML,
  validateInvoice,
  exportInvoice,
  generateBatchInvoices,
  createPlugin,
  builtInPlugins
} from 'invoice-craft';

class InvoiceManager {
  constructor(containerId) {
    this.container = document.getElementById(containerId);
    this.setupEventListeners();
  }

  setupEventListeners() {
    // Generate PDF
    this.container.querySelector('#generate-pdf').addEventListener('click',
      this.handleGeneratePDF.bind(this)
    );

    // Generate Preview
    this.container.querySelector('#generate-preview').addEventListener('click',
      this.handleGeneratePreview.bind(this)
    );

    // Export formats
    this.container.querySelector('#export-html').addEventListener('click',
      () => this.handleExport('html')
    );
    this.container.querySelector('#export-json').addEventListener('click',
      () => this.handleExport('json')
    );
  }

  async handleGeneratePDF() {
    const button = this.container.querySelector('#generate-pdf');
    const originalText = button.textContent;

    try {
      button.textContent = 'Generating...';
      button.disabled = true;

      // Validate first
      const validation = validateInvoice(this.getInvoiceData());
      if (!validation.isValid) {
        this.showErrors(validation.errors);
        return;
      }

      // Generate with plugins
      const { blob, filename } = await generateInvoicePdf(this.getInvoiceData(), {
        brandColor: '#3b82f6',
        layoutStyle: 'modern',
        plugins: [
          builtInPlugins.currencyFormatter,
          builtInPlugins.dateValidator,
          this.createCustomPlugin()
        ]
      });

      this.downloadFile(blob, filename);
      this.showSuccess(`Generated ${filename}`);

    } catch (error) {
      this.showError(error.message);
    } finally {
      button.textContent = originalText;
      button.disabled = false;
    }
  }

  async handleGeneratePreview() {
    try {
      const html = generatePreviewHTML(this.getInvoiceData(), {
        theme: 'light',
        responsive: true,
        includeStyles: true
      });

      // Show in modal or iframe
      this.showPreview(html);

    } catch (error) {
      this.showError(error.message);
    }
  }

  async handleExport(format) {
    try {
      const result = await exportInvoice(this.getInvoiceData(), {
        format,
        includeStyles: format === 'html'
      });

      this.downloadExport(result);

    } catch (error) {
      this.showError(error.message);
    }
  }

  createCustomPlugin() {
    return createPlugin({
      name: 'timestamp-plugin',
      beforeRender: (invoice) => {
        invoice.notes = `${invoice.notes}\n\nGenerated on ${new Date().toLocaleString()}`;
        return invoice;
      }
    });
  }

  downloadFile(blob, filename) {
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = filename;
    a.click();
    URL.revokeObjectURL(url);
  }

  downloadExport(result) {
    let url;
    if (result.data instanceof Blob) {
      url = URL.createObjectURL(result.data);
    } else {
      const blob = new Blob([result.data], { type: result.mimeType });
      url = URL.createObjectURL(blob);
    }

    const a = document.createElement('a');
    a.href = url;
    a.download = result.filename;
    a.click();
    URL.revokeObjectURL(url);
  }

  showPreview(html) {
    const modal = document.createElement('div');
    modal.style.cssText = `
      position: fixed; top: 0; left: 0; right: 0; bottom: 0;
      background: rgba(0,0,0,0.8); z-index: 1000;
      display: flex; align-items: center; justify-content: center;
    `;

    const iframe = document.createElement('iframe');
    iframe.srcdoc = html;
    iframe.style.cssText = 'width: 90%; height: 90%; border: none; background: white;';

    modal.appendChild(iframe);
    modal.addEventListener('click', (e) => {
      if (e.target === modal) modal.remove();
    });

    document.body.appendChild(modal);
  }

  getInvoiceData() {
    // Get invoice data from form or state
    return {
      from: {
        name: "Your Company",
        address: "123 Business St\nCity, State 12345",
        email: "contact@company.com"
      },
      to: {
        name: "Client Name",
        address: "456 Client Ave\nClient City, State 67890"
      },
      invoiceNumber: "INV-001",
      invoiceDate: new Date().toISOString().split('T')[0],
      items: [
        {
          description: "Service",
          quantity: 1,
          unitPrice: 100.00,
          taxRate: 0.1
        }
      ],
      currency: "USD"
    };
  }

  showSuccess(message) {
    this.showNotification(message, 'success');
  }

  showError(message) {
    this.showNotification(message, 'error');
  }

  showErrors(errors) {
    const messages = errors.map(e => `${e.field}: ${e.message}`).join('\n');
    this.showNotification(messages, 'error');
  }

  showNotification(message, type) {
    const notification = document.createElement('div');
    notification.textContent = message;
    notification.style.cssText = `
      position: fixed; top: 20px; right: 20px; z-index: 1001;
      padding: 12px 20px; border-radius: 6px; color: white;
      background: ${type === 'success' ? '#10b981' : '#ef4444'};
    `;

    document.body.appendChild(notification);
    setTimeout(() => notification.remove(), 3000);
  }
}

// Initialize
document.addEventListener('DOMContentLoaded', () => {
  new InvoiceManager('invoice-container');
});

// Batch processing example
async function processBatchInvoices(invoices) {
  try {
    const result = await generateBatchInvoices(invoices, {
      concurrency: 3,
      onProgress: (completed, total) => {
        console.log(`Progress: ${completed}/${total}`);
        updateProgressBar(completed / total * 100);
      },
      onError: (error, invoice, index) => {
        console.error(`Failed to process invoice ${index}:`, error);
      }
    });

    console.log(`Batch completed: ${result.summary.successful} successful, ${result.summary.failed} failed`);

    // Download all successful PDFs
    result.success.forEach((item, index) => {
      setTimeout(() => {
        const url = URL.createObjectURL(item.blob);
        const a = document.createElement('a');
        a.href = url;
        a.download = item.filename;
        a.click();
        URL.revokeObjectURL(url);
      }, index * 500);
    });

  } catch (error) {
    console.error('Batch processing failed:', error);
  }
}

function updateProgressBar(percentage) {
  const progressBar = document.getElementById('progress-bar');
  if (progressBar) {
    progressBar.style.width = `${percentage}%`;
  }
}

Node.js Server Usage

// server.js
import express from 'express';
import { generateInvoicePdf, validateInvoice } from 'invoice-craft';
import fs from 'fs/promises';

const app = express();
app.use(express.json());

// Generate invoice endpoint
app.post('/api/invoices/generate', async (req, res) => {
  try {
    const invoiceData = req.body;

    // Validate invoice data
    const validation = validateInvoice(invoiceData);
    if (!validation.isValid) {
      return res.status(400).json({
        error: 'Invalid invoice data',
        details: validation.errors
      });
    }

    // Generate PDF
    const { blob, filename } = await generateInvoicePdf(invoiceData, {
      brandColor: '#3b82f6',
      layoutStyle: 'modern'
    });

    // Convert blob to buffer for Node.js
    const buffer = Buffer.from(await blob.arrayBuffer());

    // Save to file system (optional)
    await fs.writeFile(`./invoices/${filename}`, buffer);

    // Send as response
    res.setHeader('Content-Type', 'application/pdf');
    res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
    res.send(buffer);

  } catch (error) {
    console.error('Invoice generation failed:', error);
    res.status(500).json({ error: 'Internal server error' });
  }
});

// Batch generation endpoint
app.post('/api/invoices/batch', async (req, res) => {
  try {
    const { invoices } = req.body;

    const result = await generateBatchInvoices(invoices, {
      concurrency: 5,
      continueOnError: true
    });

    // Save all successful PDFs
    const savedFiles = [];
    for (const item of result.success) {
      const buffer = Buffer.from(await item.blob.arrayBuffer());
      await fs.writeFile(`./invoices/${item.filename}`, buffer);
      savedFiles.push(item.filename);
    }

    res.json({
      success: true,
      summary: result.summary,
      files: savedFiles,
      errors: result.errors.map(e => ({
        index: e.index,
        message: e.error.message
      }))
    });

  } catch (error) {
    console.error('Batch generation failed:', error);
    res.status(500).json({ error: 'Internal server error' });
  }
});

app.listen(3000, () => {
  console.log('Invoice server running on port 3000');
});

πŸ“‹ Complete API Reference

Core Interfaces

interface InvoiceData {
  from: {
    name: string;
    address: string;
    email?: string;
    phone?: string;
    logoUrl?: string;
    brandColor?: string;
  };
  to: {
    name: string;
    address: string;
    email?: string;
    phone?: string;
  };
  invoiceNumber: string;
  invoiceDate: string;        // YYYY-MM-DD format
  dueDate?: string;          // YYYY-MM-DD format
  items: InvoiceItem[];
  currency: string;          // ISO currency code (USD, EUR, etc.)
  terms?: string;
  notes?: string;
}

interface InvoiceItem {
  description: string;
  quantity: number;
  unitPrice: number;
  taxRate?: number;         // 0.1 for 10% tax
}

interface GeneratePdfOptions {
  brandColor?: string;
  logoUrl?: string;
  layoutStyle?: 'default' | 'modern' | 'minimal' | 'creative';
  labels?: Partial<Labels>;
  validate?: (invoice: InvoiceData) => void | ValidationResult;
  filenameTemplate?: (context: { invoice: InvoiceData }) => string;
  plugins?: InvoicePlugin[];
  customTemplate?: CustomTemplate;
  exportOptions?: ExportOptions;
}

Export & Validation

interface ExportOptions {
  format?: 'pdf' | 'html' | 'json' | 'csv';
  compression?: boolean;
  quality?: 'low' | 'medium' | 'high';
  includeStyles?: boolean;
  brandColor?: string;
  logoUrl?: string;
  layoutStyle?: 'default' | 'modern' | 'minimal' | 'creative';
}

interface ValidationResult {
  isValid: boolean;
  errors: ValidationError[];
  warnings: ValidationError[];
}

interface ValidationError {
  field: string;
  message: string;
  code: string;
  severity: 'error' | 'warning';
}

Batch Processing

interface BatchOptions {
  concurrency?: number;
  onProgress?: (completed: number, total: number) => void;
  onError?: (error: Error, invoice: InvoiceData, index: number) => void;
  continueOnError?: boolean;
}

interface BatchResult {
  success: Array<{ blob: Blob; filename: string; index: number }>;
  errors: Array<{ error: Error; invoice: InvoiceData; index: number }>;
  summary: {
    total: number;
    successful: number;
    failed: number;
  };
}

Template System

interface CustomTemplate {
  id: string;
  name: string;
  description?: string;
  header: TemplateSection;
  body: TemplateSection;
  footer: TemplateSection;
  styles: TemplateStyles;
  supportedFeatures?: {
    logo?: boolean;
    brandColor?: boolean;
    rtl?: boolean;
    extraSections?: boolean;
  };
}

interface TemplateSection {
  content: string | ((data: any) => string);
  styles?: TemplateStyles;
  visible?: boolean | ((data: any) => boolean);
}

Plugin System

interface InvoicePlugin {
  name: string;
  version?: string;
  beforeRender?: (invoice: InvoiceData) => InvoiceData | Promise<InvoiceData>;
  afterRender?: (pdf: any) => any | Promise<any>;
  beforeValidation?: (invoice: InvoiceData) => InvoiceData | Promise<InvoiceData>;
  afterValidation?: (result: ValidationResult) => ValidationResult | Promise<ValidationResult>;
}

πŸ’‘ Common Usage Patterns

Integration Examples

Use Case Key Features Implementation
E-commerce Order β†’ Invoice, Auto-validation Transform order data, validate, generate
SaaS Billing Batch processing, Multiple formats Monthly billing, API endpoints
Multi-tenant Custom branding, Plugins Tenant-specific templates, dynamic branding
Accounting Validation, Export formats Strict validation, CSV/JSON export
Freelancing Templates, Customization Personal branding, custom layouts

Quick Implementation

// E-commerce: Order to Invoice
const invoiceData = transformOrderToInvoice(order, customer);
const { blob } = await generateInvoicePdf(invoiceData, {
  brandColor: process.env.BRAND_COLOR,
  layoutStyle: 'modern'
});

// SaaS: Batch Billing
const result = await generateBatchInvoices(subscriptions, {
  concurrency: 10,
  onProgress: (completed, total) => updateProgress(completed, total)
});

// Multi-tenant: Custom Branding
const tenantPlugin = createPlugin({
  name: 'tenant-branding',
  beforeRender: (invoice) => applyTenantBranding(invoice, tenantId)
});

οΏ½ Advanced Features

Feature Overview

Feature Function Purpose Example
HTML Preview generatePreviewHTML() Live preview without PDF generatePreviewHTML(data, {theme: 'light'})
Export Formats exportInvoice() PDF, HTML, JSON, CSV exportInvoice(data, {format: 'html'})
Batch Processing generateBatchInvoices() Multiple invoices generateBatchInvoices(invoices, {concurrency: 5})
Validation validateInvoice() Data validation validateInvoice(data)
Plugins createPlugin() Custom functionality createPlugin({name: 'custom'})
Templates createTemplate() Custom layouts createTemplate('id', 'name')

Quick Examples

// HTML Preview
const html = generatePreviewHTML(invoiceData, {theme: 'light'});

// Export Formats
const result = await exportInvoice(invoiceData, {format: 'json'});

// Batch Processing
const batch = await generateBatchInvoices(invoices, {
  concurrency: 3,
  onProgress: (done, total) => console.log(`${done}/${total}`)
});

// Validation
const validation = validateInvoice(invoiceData);
if (!validation.isValid) console.log(validation.errors);

// Custom Plugin
const plugin = createPlugin({
  name: 'timestamp',
  beforeRender: (invoice) => {
    invoice.notes += `\nGenerated: ${new Date().toLocaleString()}`;
    return invoice;
  }
});

// Custom Template
const template = createTemplate('modern', 'Modern Design')
  .setHeader(data => `<h1>${data.invoice.from.name}</h1>`)
  .build();

🎯 Advanced Examples

Custom Filename Generation

const options = {
  filenameTemplate: ({ invoice }) =>
    `invoice-${invoice.invoiceNumber}-${invoice.to.name.replace(/\s+/g, '-')}.pdf`
};

const { blob, filename } = await generateInvoicePdf(invoiceData, options);
// filename: "invoice-INV-001-Client-Name.pdf"

Error Handling

try {
  const { blob, filename } = await generateInvoicePdf(invoiceData);
  // Success - handle the PDF
} catch (error) {
  if (error.message.includes('validation')) {
    // Handle validation errors
    console.error('Invalid invoice data:', error.message);
  } else {
    // Handle other errors
    console.error('PDF generation failed:', error.message);
  }
}

Batch Export with Progress

import { exportBatchInvoices } from 'invoice-craft';

const results = await exportBatchInvoices(invoices, {
  format: 'pdf',
  concurrency: 5,
  onProgress: (completed, total) => {
    const percentage = ((completed / total) * 100).toFixed(1);
    console.log(`Progress: ${completed}/${total} (${percentage}%)`);
  }
});

// Download all successful exports
results.success.forEach(result => {
  const url = URL.createObjectURL(result.blob);
  const a = document.createElement('a');
  a.href = url;
  a.download = result.filename;
  a.click();
  URL.revokeObjectURL(url);
});

πŸ› οΈ Development

Building from Source

git clone https://github.com/yourusername/invoice-craft.git
cd invoice-craft
npm install
npm run build

Running Tests

npm test

πŸ“„ License

MIT License - see LICENSE file for details.

🀝 Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

πŸ“ž Support


Made with ❀️ for developers who need reliable invoice PDF generation.

About

White-label invoice PDF generator for any frontend - React, Vue, Angular, vanilla JS support

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published