Untitled

 avatar
unknown
typescript
21 days ago
15 kB
3
Indexable
import fs from "fs";
import path from "path";
import PDFDocument from "pdfkit";

interface Invoice {
  invoice?: string;
  billTo?: string;
  billToInfo?: any;
  shipTo?: string;
  shipToInfo?: any;
  paymentDate?: string;
  paymentDateInfo?: string;
  paymentTerms?: string;
  calculationModes: any;
  paymentTermsInfo?: string;
  currency?: string;
  dueDate?: string;
  dueDateInfo?: string;
  poNumber?: string;
  poNumberInfo?: string;
  amountDue?: string;
  calc?: {
    due?: number;
    subTotal?: number;
    grandTotal: any;
  };
  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;
  waterMarkOpacity: number;
  discount?: string;
  discountInfo?: number;
  total?: string;
  totalInfo?: number;
  amountPaid?: string;
  amountPaidInfo?: number;
  note?: string;
  noteInfo?: string;
  terms?: string;
  termsInfo?: string;
  invoiceType: "default" | "custom";
  logo?: string;
  header?: string;
  footer?: string;
  watermark?: string;
  invoiceNum?: string;
  status: string;
}

async function createCustomInvoice(
  invoice: Invoice,
  outputPath: string
): Promise<void> {
  if (!invoice || typeof invoice !== "object") {
    throw new Error("Invalid input invoice provided.");
  }

  let mInvoice = { ...invoice };

  ["user", "createdAt", "updatedAt", "id", "type"].forEach((key) => {
    delete mInvoice?.billToInfo?.[key];
    delete mInvoice?.shipToInfo?.[key];
  });

  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 {
  if (invoice?.header) {
    doc.image(
      path.join(process.cwd(), "files", "images", invoice.header),
      0,
      0,
      { width: 600, height: 80 }
    );
  }
  if (invoice?.footer)
    doc.image(
      path.join(process.cwd(), "files", "images", invoice.footer),
      0,
      805,
      { width: 600, fit: [100, 100] }
    );
  if (invoice?.watermark)
    doc.opacity(Number(invoice?.waterMarkOpacity) / 100 || 1);
  if (invoice?.watermark) {
    doc.image(
      path.join(process.cwd(), "files", "images", invoice?.watermark || ""),
      163,
      290,
      { width: 270, fit: [100, 100] }
    );
  }
  doc.opacity(100 / 100);

  // if (invoice?.invoice) {
  //   doc
  //     .fillColor("#000000")
  //     .fontSize(24)
  //     .text(`${invoice?.invoice}`, 0, 75, { align: "right" })
  //     .moveDown();
  // }
  // if (invoice?.invoiceNum) {
  //   doc
  //     .fillColor("#000000")
  //     .fontSize(12)
  //     .text(`${invoice?.invoiceNum}`, 0, 100, { 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 = 6;

  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 {
        // @ts-ignore
        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(
        formatDateDDMMYYYY(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(formatDateDDMMYYYY(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 = currentY - 6.1;
  const rectWidth = 185;
  const rectHeight = 20;
  const roundingRadius = 2;
  doc.roundedRect(390, stockY, rectWidth, rectHeight, roundingRadius);
  doc.fill(invoice.headerBgColor);
  doc.font("Helvetica-Bold");
  doc.fillColor(`${invoice?.headerTextColor}`);

  if (!isEmpty(invoice?.amountDue) && !isEmpty(invoice.calc?.due)) {
    doc
      .font("Helvetica-Bold")
      .text(`${invoice?.subtotal}`, 0, currentY, {
        width: 470,
        align: "right",
      })
      .text(`${invoice?.currency} ${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.toString(),
      `${invoice.currency} ${item.rate.toString()}`,
      `${invoice.currency} ${item.quantity * item.rate}`.toString(),
      "center"
    );
  }

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

  let currentPosition = subtotalPosition + 20;

  if (invoice.shippingInfo) {
    generateTableInfo(
      doc,
      currentPosition,
      "",
      "",
      `${invoice.shipping} (${invoice.shippingInfo} ${invoice.calculationModes.shipping})`,
      formatCurrency(invoice.shippingInfo || 0, invoice?.currency || "$")
    );
    currentPosition += 20;
  }

  if (invoice.taxInfo) {
    generateTableInfo(
      doc,
      currentPosition,
      "",
      "",
      `${invoice.tax} (${invoice.taxInfo} ${invoice.calculationModes.tax})`,
      formatCurrency(invoice.taxInfo || 0, invoice?.currency || "$")
    );
    currentPosition += 20;
  }
  if (invoice.discountInfo) {
    generateTableInfo(
      doc,
      currentPosition,
      "",
      "",
      `${invoice.discount} (${invoice.discountInfo} ${invoice.calculationModes.discount})`,
      formatCurrency(invoice.discountInfo || 0, invoice?.currency || "$")
    );
    currentPosition += 20;
  }
  if (invoice.calc?.grandTotal) {
    generateTableInfo(
      doc,
      currentPosition,
      "",
      "",
      `${invoice.total}`,
      formatCurrency(invoice.calc?.grandTotal || 0, invoice?.currency || "$")
    );
    currentPosition += 20;
  }

  if (invoice.amountPaidInfo) {
    generateTableInfo(
      doc,
      currentPosition,
      "",
      "",
      `${invoice.amountPaid}`,
      formatCurrency(
        invoice.status === "PAID"
          ? invoice.calc?.grandTotal
          : invoice.amountPaidInfo || 0,
        invoice?.currency || "$"
      )
    );
    currentPosition += 20;
  }

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

  generateTableInfo(
    doc,
    duePosition,
    "",
    "",
    `${invoice.amountDue}`,
    formatCurrency(
      invoice.status === "PAID" ? 0 : invoice.calc?.due || 0,
      invoice?.currency || "$"
    )
  );

  const tableHeight = duePosition + 20;
  if (invoice.status === "PAID") {
    doc.opacity(50 / 100 || 1);
    doc.image("static/paid.png", 460, tableHeight + 40, {
      fit: [150, 60],
    });
  }
  doc.opacity(100 / 100 || 1);
  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;

  // Calculate the height required for the note and noteInfo
  const noteHeight = doc.heightOfString(`${invoice?.noteInfo}`, {
    width: 500,
  });

  // Check if there is enough space on the current page for the note and noteInfo
  if (notePosition + noteHeight + 50 > doc.page.height) {
    doc.addPage();
    notePosition = 50; // Reset the position to the top of the new page
  }

  if (invoice.noteInfo) {
    doc.fontSize(10).text(`${invoice.note}`, 25, notePosition);

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

  let termsPosition = notePosition + noteHeight + 20;

  // Check if there is enough space on the current page for the terms and termsInfo
  if (termsPosition + 50 > doc.page.height) {
    doc.addPage();
    termsPosition = 50; // Reset the position to the top of the new page
  }

  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, currency?: string): string {
  return `${currency === "" ? "" : currency} ${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}`;
// }

function formatDateDDMMYYYY(date: Date): string {
  const day = String(date.getDate()).padStart(2, "0");
  const month = String(date.getMonth() + 1).padStart(2, "0");
  const year = date.getFullYear();

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

export { createCustomInvoice };
Editor is loading...
Leave a Comment