Untitled

 avatar
unknown
plain_text
4 days ago
12 kB
4
Indexable
import PDFDocument from 'pdfkit';
import fs from 'fs';

interface Invoice {
  invoice?: string;
  billTo?: string;
  billToInfo?: any;
  shipTo?: string;
  shipToInfo?: any;
  paymentDate?: string;
  paymentDateInfo?: string;
  paymentTerms?: string;
  paymentTermsInfo?: string;
  dueDate?: string;
  dueDateInfo?: string;
  poNumber?: string;
  poNumberInfo?: string;
  amountDue?: string;
  calc?: {
    due?: number;
    subTotal?: number;
  };
  headerBgColor?: string;
  headerTextColor?: string;
  item?: string;
  quantity?: string;
  rate?: string;
  amount?: string;
  rows?: Array<{
    title: string;
    quantity: number;
    rate: number;
  }>;
  subtotal?: string;
  shipping?: string;
  shippingInfo?: number;
  tax?: string;
  taxInfo?: number;
  discount?: string;
  discountInfo?: number;
  total?: string;
  totalInfo?: number;
  amountPaid?: string;
  amountPaidInfo?: number;
  note?: string;
  noteInfo?: string;
  terms?: string;
  termsInfo?: string;
}

function createInvoice(invoice: Invoice, outputPath: string): void {
  if (!invoice || typeof invoice !== 'object') {
    throw new Error('Invalid input invoice provided.');
  }

  const doc = new PDFDocument({ margin: 35, size: 'A4' });
  doc.pipe(fs.createWriteStream(outputPath));

  doc.font('Helvetica');

  generateHeader(doc, invoice);

  const dynamicSectionsEndY = generateDynamicSections(doc, invoice, 50, 140);

  const tableStartY = Math.max(dynamicSectionsEndY, 300);
  generateInvoiceDetails(doc, invoice);
  generateInvoiceTable(doc, invoice, tableStartY);

  doc.end();
}

function generateHeader(doc: PDFKit.PDFDocument, invoice: Invoice): void {
  doc.image('bg-new.png', 0, 0, { width: 600, fit: [100, 100] });
  doc.image('invoice footer-bg.png', 0, 815, { width: 600, fit: [100, 100] });
  doc
    .fillColor('#000000')
    .fontSize(10)
    .fontSize(12)
    .text(`${invoice?.invoice}`, 20, 86, { align: 'right' })
    .fontSize(14)
    .moveDown();
}

function generateDynamicSections(doc: PDFKit.PDFDocument, data: Invoice, startX: number, startY: number): number {
  const sectionSpacing = 152;
  let currentX = startX;
  let maxY = startY;
  const extraSpacing = 4;

  const flattenObject = (obj: any, parentKey = ''): any => {
    return Object.keys(obj).reduce((acc, key) => {
      const newKey = parentKey ? `${parentKey}.${key}` : key;
      if (typeof obj[key] === 'object' && obj[key] !== null) {
        Object.assign(acc, flattenObject(obj[key], newKey));
      } else {
        acc[newKey] = obj[key];
      }
      return acc;
    }, {});
  };

  const billToInfo = flattenObject(data.billToInfo);
  let currentY = startY;

  doc
    .fillColor('#666666')
    .font('Helvetica')
    .fontSize(11)
    .text(`${data.billTo}`, currentX - 20, currentY);
  currentY += 15;

  Object.keys(billToInfo).forEach((key) => {
    const value = billToInfo[key];
    const wrappedText = `${key.charAt(0).toUpperCase() + key.slice(1)}: ${value}`;
    doc
      .fillColor('#333333')
      .font('Helvetica')
      .fontSize(10)
      .text(wrappedText, currentX - 20, currentY, {
        width: 160,
        lineBreak: true,
      });

    const textHeight = doc.heightOfString(wrappedText, { width: 160 });
    currentY += textHeight + extraSpacing;
  });

  maxY = Math.max(maxY, currentY);
  currentX += sectionSpacing;

  const shipToInfo = flattenObject(data.shipToInfo);
  currentY = startY;

  doc
    .fillColor('#666666')
    .font('Helvetica')
    .fontSize(11)
    .text(`${data.shipTo}`, currentX, currentY);
  currentY += 15;

  Object.keys(shipToInfo).forEach((key) => {
    const value = shipToInfo[key];
    const wrappedText = `${key.charAt(0).toUpperCase() + key.slice(1)}: ${value}`;
    doc
      .fillColor('#333333')
      .font('Helvetica')
      .fontSize(10)
      .text(wrappedText, currentX, currentY, {
        width: 160,
        lineBreak: true,
      });

    const textHeight = doc.heightOfString(wrappedText, { width: 160 });
    currentY += textHeight + extraSpacing;
  });

  maxY = Math.max(maxY, currentY);
  return maxY;
}

function generateInvoiceDetails(doc: PDFKit.PDFDocument, invoice: Invoice): void {
  const detailsTop = 130;
  let currentY = detailsTop;

  const isEmpty = (value: any): boolean => {
    return value === null || value === undefined || value === '';
  };

  if (!isEmpty(invoice?.paymentDate) && !isEmpty(invoice.paymentDateInfo)) {
    doc
      .fontSize(10)
      .fillColor('#666666')
      .text(`${invoice.paymentDate}`, 0, currentY, {
        width: 470,
        align: 'right',
      })
      .fillColor('#000000')
      .text(formatDate(new Date(invoice.paymentDateInfo)), 0, currentY, {
        width: 550,
        align: 'right',
      });
    currentY += 25;
  }

  if (!isEmpty(invoice?.paymentTerms) && !isEmpty(invoice.paymentTermsInfo)) {
    doc
      .fontSize(10)
      .fillColor('#666666')
      .text(`${invoice.paymentTerms}`, 0, currentY, {
        width: 470,
        align: 'right',
      })
      .fillColor('#000000')
      .text(`${invoice.paymentTermsInfo}`, 0, currentY, {
        width: 550,
        align: 'right',
      });
    currentY += 25;
  }

  if (!isEmpty(invoice?.dueDate) && !isEmpty(invoice.dueDateInfo)) {
    doc
      .fontSize(10)
      .fillColor('#666666')
      .text(`${invoice.dueDate}`, 0, currentY, {
        width: 470,
        align: 'right',
      })
      .fillColor('#000000')
      .text(formatDate(new Date(invoice.dueDateInfo)), 0, currentY, {
        width: 550,
        align: 'right',
      });
    currentY += 25;
  }

  if (!isEmpty(invoice?.poNumber) && !isEmpty(invoice.poNumberInfo)) {
    doc
      .fontSize(10)
      .fillColor('#666666')
      .text(`${invoice.poNumber}`, 0, currentY, {
        width: 470,
        align: 'right',
      })
      .fillColor('#000000')
      .text(`${invoice.poNumberInfo}`, 0, currentY, {
        width: 550,
        align: 'right',
      });
    currentY += 25;
  }

  let stockY = 198.5;
  const rectWidth = 185;
  const rectHeight = 20;
  const roundingRadius = 2;

  doc.roundedRect(390, stockY, rectWidth, rectHeight, roundingRadius);
  doc.fill(invoice.headerBgColor?.slice(0, 7));
  doc.font('Helvetica-Bold');
  doc.fillColor(`${invoice.headerTextColor.slice(0, 7)}`);

  if (!isEmpty(invoice?.amountDue) && !isEmpty(invoice.calc?.due)) {
    doc
      .font('Helvetica-Bold')
      .text(`${invoice.amountDue}`, 0, currentY, {
        width: 470,
        align: 'right',
      })
      .text(`${invoice.calc.due}`, 0, currentY, {
        width: 550,
        align: 'right',
      });
  }
}

function generateInvoiceTable(doc: PDFKit.PDFDocument, invoice: Invoice, tableStartY: number): void {
  let i;
  const invoiceTableTop = 320;
  let currentY = 308.5;
  const rectWidth = 555;
  const rectHeight = 20;
  const roundingRadius = 2;

  doc
    .roundedRect(20, currentY, rectWidth, rectHeight, roundingRadius)
    .fill(`${invoice.headerBgColor}`);
  doc.font('Helvetica-Bold');
  doc.fillColor(`${invoice.headerTextColor}`);
  generateTableRow(
    doc,
    invoiceTableTop - 5,
    `${invoice.item}`,
    `${invoice.quantity}`,
    `${invoice.rate}`,
    `${invoice.amount}`
  );
  doc.fillColor('#000000');
  doc.font('Helvetica');

  for (i = 0; i < invoice.rows!.length; i++) {
    const item = invoice.rows![i];
    const position = invoiceTableTop + (i + 1) * 24;
    if (position > doc.page.height - 50) {
      doc.addPage();
      currentY = 50;
    }
    generateTableRow(
      doc,
      position,
      item.title,
      item.quantity,
      item.rate,
      item.quantity * item.rate,
      'center'
    );
  }

  const subtotalPosition = invoiceTableTop + (i + 1) * 27;
  generateHr(doc, subtotalPosition - 15);
  generateTableInfo(
    doc,
    subtotalPosition,
    '',
    '',
    `${invoice.subtotal}`,
    formatCurrency(invoice.calc?.subTotal || 0)
  );

  let currentPosition = subtotalPosition + 20;

  if (invoice.shippingInfo) {
    generateTableInfo(
      doc,
      currentPosition,
      '',
      '',
      `${invoice.shipping}`,
      formatCurrency(invoice.shippingInfo)
    );
    currentPosition += 20;
  }

  if (invoice.taxInfo) {
    generateTableInfo(
      doc,
      currentPosition,
      '',
      '',
      `${invoice.tax}`,
      formatCurrency(invoice.taxInfo)
    );
    currentPosition += 20;
  }
  if (invoice.discountInfo) {
    generateTableInfo(
      doc,
      currentPosition,
      '',
      '',
      `${invoice.discount}`,
      formatCurrency(invoice.discountInfo)
    );
    currentPosition += 20;
  }
  if (invoice.totalInfo) {
    generateTableInfo(
      doc,
      currentPosition,
      '',
      '',
      `${invoice.total}`,
      formatCurrency(invoice.totalInfo)
    );
    currentPosition += 20;
  }
  if (invoice.amountPaidInfo) {
    generateTableInfo(
      doc,
      currentPosition,
      '',
      '',
      `${invoice.amountPaid}`,
      formatCurrency(invoice.amountPaidInfo)
    );
    currentPosition += 20;
  }

  const duePosition = currentPosition;
  doc.font('Helvetica-Bold');

  generateTableInfo(
    doc,
    duePosition,
    '',
    '',
    `${invoice.amountDue}`,
    formatCurrency(invoice.calc?.due || 0)
  );

  const tableHeight = duePosition + 20;

  generateNotesAndTerms(doc, invoice, tableHeight);
}

function generateTableRow(
  doc: PDFKit.PDFDocument,
  y: number,
  item: string,
  unitCost: string,
  quantity: string,
  lineTotal: string,
  align: string = 'right'
): void {
  doc
    .fontSize(10)
    .text(item, 30, y)
    .text(unitCost, 320, y, { width: 90, align: 'center' })
    .text(quantity, 420, y, { width: 90, align: 'center' })
    .text(lineTotal, 470, y, { align: 'right' });
}

function generateTableInfo(
  doc: PDFKit.PDFDocument,
  y: number,
  item: string,
  unitCost: string,
  quantity: string,
  lineTotal: string
): void {
  doc
    .fontSize(10)
    .text(item, 30, y)
    .text(unitCost, 240, y, { width: 90, align: 'right' })
    .text(quantity, 360, y, { width: 90, align: 'right' })
    .text(lineTotal, 470, y, { align: 'right' });
}

function generateNotesAndTerms(doc: PDFKit.PDFDocument, invoice: Invoice, tableHeight: number): void {
  let notePosition = tableHeight + 20;
  const lorem1Height = doc.heightOfString(`${invoice?.noteInfo}`, {
    width: 500,
  });
  if (invoice.noteInfo) {
    doc.fontSize(10).text(`${invoice.note}`, 25, notePosition);

    doc.font('Helvetica').text(`${invoice.noteInfo}`, 25, notePosition + 15);
  }

  const termsPosition = notePosition + lorem1Height + 20;

  if (invoice.termsInfo) {
    doc
      .fontSize(10)
      .font('Helvetica-Bold')
      .text(`${invoice.terms}`, 25, termsPosition);

    doc.font('Helvetica').text(`${invoice.termsInfo}`, 25, termsPosition + 15);
  }
}

function formatCurrency(amount: number): string {
  return `$${Number(amount)?.toFixed(2)}`;
}

function generateHr(doc: PDFKit.PDFDocument, y: number): void {
  doc
    .strokeColor('#06243E')
    .lineWidth(0.1)
    .moveTo(27, y)
    .lineTo(562, y)
    .stroke();
}

function formatDate(date: Date): string {
  const day = date.getDate();
  const month = date.getMonth() + 1;
  const year = date.getFullYear();

  return `${year}/${month}/${day}`;
}

export { createInvoice };
Leave a Comment