DEV Community

Cover image for Creating a dynamic invoice using react-print-pdf
Auguste for Onedoc

Posted on

Creating a dynamic invoice using react-print-pdf

Designing the document

First, let's model the information we would like our invoice to showcase.

  • Let's model your business and your customer as a Party
  • The Item as an element for which the customer should be charged for in the invoice.
  • The InvoiceProps object as the invoice itself.
export interface Party {
  name: string;
  address: string;
  city: string;
  ZIPCode: string;
  country: string;
}

export interface Item {
  name: string;
  description?: string;
  price: number;
  quantity: number;
  total: number;
}

export interface InvoiceProps {
  id: number;
  buyer: Party;
  seller: Party;
  date: Date;
  dateString: string;
  items: Item[];
}
Enter fullscreen mode Exit fullscreen mode

Next, let's build the React components that constitute our invoice. The components make use of the dataModels we previously designed to leverage typescript's typing capabilities, making our doucment dynamic and structured.

import React from "react";
import { Party, Item } from "./dataModels";

export const Logo = (props) => {
  return (
    <svg
      xmlns="http://www.w3.org/2000/svg"
      x="0"
      y="0"
      enableBackground="new 0 0 46.15 9.31"
      version="1.1"
      viewBox="0 0 46.15 9.31"
      xmlSpace="preserve"
      fill="black"
      {...props}
    >
      <path d="M10 9.13V2.55h1.83v.91c.35-.62 1.13-1.09 2.07-1.09.71 0 1.32.24 1.81.71s.74 1.15.74 2.03v4.02h-1.88V5.6c0-.96-.5-1.5-1.28-1.5-.85 0-1.42.62-1.42 1.55v3.48H10zM23.84 6.48h-4.83c.23.83.83 1.24 1.79 1.24.74 0 1.43-.22 2.05-.64l.74 1.28c-.8.61-1.76.91-2.88.91-1.16 0-2.05-.34-2.67-1-.61-.66-.92-1.47-.92-2.45 0-1 .32-1.81.96-2.46.64-.66 1.48-.98 2.51-.98.97 0 1.76.3 2.39.89.62.59.94 1.39.94 2.41-.01.23-.04.5-.08.8zM19 5.13h3.09c-.18-.76-.73-1.22-1.51-1.22-.76 0-1.38.46-1.58 1.22zM29.43 0h1.88v9.13h-1.82v-.71c-.52.59-1.16.88-1.96.88-.92 0-1.69-.32-2.31-.98-.61-.66-.92-1.47-.92-2.47 0-.98.31-1.8.92-2.46.62-.66 1.39-1 2.31-1 .74 0 1.38.26 1.89.8V0zm-.39 4.6c-.31-.34-.71-.5-1.2-.5s-.89.17-1.21.5c-.31.34-.47.74-.47 1.22 0 .49.16.91.47 1.25.32.34.72.5 1.21.5s.89-.17 1.2-.5c.32-.34.48-.76.48-1.25 0-.47-.15-.88-.48-1.22zM33.03 8.31c-.66-.67-.98-1.5-.98-2.47s.32-1.8.98-2.46c.66-.67 1.51-1.01 2.55-1.01 1.04 0 1.91.34 2.57 1.01.66.66 1 1.49 1 2.46s-.34 1.8-1 2.47c-.66.66-1.52 1-2.57 1-1.04 0-1.89-.34-2.55-1zm3.74-3.68c-.32-.34-.72-.5-1.19-.5s-.86.17-1.19.5c-.32.32-.48.73-.48 1.2 0 .49.16.9.48 1.24.32.32.72.49 1.19.49s.86-.17 1.19-.49c.32-.34.49-.74.49-1.24 0-.47-.17-.88-.49-1.2zM40.5 8.31c-.65-.65-.97-1.47-.97-2.48s.32-1.83.98-2.47c.66-.65 1.5-.97 2.54-.97 1.36 0 2.55.67 3.09 1.87l-1.5.8c-.38-.62-.9-.94-1.56-.94-.49 0-.89.17-1.21.49-.32.32-.48.73-.48 1.21 0 .49.16.91.47 1.24.32.32.72.48 1.2.48.66 0 1.27-.38 1.55-.92l1.52.9c-.58 1.07-1.74 1.75-3.12 1.75-1.02 0-1.86-.32-2.51-.96zM9.26 4.7c0-1.29-.44-2.36-1.34-3.25C7.03.55 5.94.1 4.63.1c-1.3 0-2.39.45-3.29 1.35C.45 2.34 0 3.43 0 4.71c0 .37.05.72.12 1.05l4.3-3.39h2.22v6.46c.47-.22.9-.5 1.29-.88.89-.89 1.33-1.97 1.33-3.25z"></path>
      <path d="M1.49 8.09c.62.56 1.34.94 2.17 1.1v-2.8l-2.17 1.7z"></path>
    </svg>
  );
};

export const Right = ({ children }) => {
  return <div className="text-right">{children}</div>;
};

export const InvoiceHeader = ({ props }) => {
  return (
    <div>
      <h1 className="text-2xl font-bold">Invoice #{props.id}</h1>
      <p className="text-xs">{props.dateString}</p>
    </div>
  );
};

export const LineSeparation = () => {
  return <div className="h-px bg-gray-300 my-4" />;
};

export const PartySection = ({
  party,
  buyer,
}: {
  party: Party;
  buyer: boolean;
}) => {
  let partyDetails = Object.entries(party).map(([key, value]) => (
    <p className="p-0 mb-1">{value}</p>
  ));

  return buyer ? (
    <div>
      <p className="p-0 mb-1">
        <b>Bill to:</b>
      </p>
      {partyDetails}
    </div>
  ) : (
    <Right>{partyDetails}</Right>
  );
};

export const ItemTable = ({ items }: { items: Item[] }) => {
  const content = items.map((a) => {
    return (
      <React.Fragment key={items.indexOf(a)}>
        <tr className="border-b border-gray-300">
          <td className="py-2">{items.indexOf(a)}</td>
          <td className="py-2">{a.name}</td>
          <td className="py-2">${a.price}</td>
          <td className="py-2">{a.quantity}</td>
          <td className="py-2">${a.total}</td>
        </tr>
      </React.Fragment>
    );
  });
  const totalPrice = items
    .map((a) => {
      return a.total;
    })
    .reduce((accumulator, currentValue) => {
      return accumulator + currentValue;
    }, 0);
  return (
    <table className="w-full my-12">
      <tr className="border-b border-gray-300">
        <th className="text-left font-bold py-2">Item</th>
        <th className="text-left font-bold py-2">Description</th>
        <th className="text-left font-bold py-2">Unit Price</th>
        <th className="text-left font-bold py-2">Quantity</th>
        <th className="text-left font-bold py-2">Amount</th>
      </tr>

      {content}

      <tr className="border-b border-gray-300">
        <th className="text-left font-bold py-2"></th>
        <th className="text-left font-bold py-2">Total</th>
        <th className="text-left font-bold py-2"></th>
        <th className="text-left font-bold py-2"></th>
        <th className="text-left font-bold py-2">${totalPrice}</th>
      </tr>
    </table>
  );
};
Enter fullscreen mode Exit fullscreen mode

Finally, let's assemble these components to design the full document our document, using our components and dataModels.

import React from "react";
import { Footnote, PageBottom, Tailwind } from "@onedoc/react-print";
import {
  PartySection,
  ItemTable,
  InvoiceHeader,
  LineSeparation,
  Logo,
} from "./components";
import { InvoiceProps } from "./dataModels";

export const Invoice = (props: InvoiceProps) => {
  return (
    <Tailwind>
      <div className="font-[arial]">
        <div className="flex justify-between items-end pb-4 mb-8">
          <InvoiceHeader props={props} />
          <Logo className="h-6" />
        </div>

        <PartySection party={props.seller} buyer={false} />
        <LineSeparation />

        <PartySection party={props.buyer} buyer={true} />
        <LineSeparation />

        <p className="p-0 leading-5">
          All items below correspond to work completed in the month of{" "}
          {props.date.toLocaleString("default", { month: "long" })}{" "}
          {props.date.getFullYear()}. Payment is due within 15 days of receipt
          of this invoice.
          <Footnote>This includes non-business days.</Footnote>
        </p>

        <ItemTable items={props.items} />

        <div className="bg-blue-100 p-3 rounded-md border-blue-300 text-blue-800 text-sm">
          On {props.dateString}, Onedoc users will be upgraded free of charge to
          our new cloud offering.
        </div>

        <PageBottom>
          <LineSeparation />
          <div className="text-gray-400 text-sm">Invoice #{props.id}</div>
        </PageBottom>
      </div>
    </Tailwind>
  );
};
Enter fullscreen mode Exit fullscreen mode

From React to PDF, how ?

The following snippets shows how easy it is to render your React document into a PDF document. You can use the compile component from @onedoc/react-print

import React from "react";
import fs from "fs";
import { Invoice } from "./invoice/invoice.tsx";
import { Onedoc } from "@onedoc/client";
import { InvoiceProps } from "./invoice/dataModels.tsx";
import { compile } from "@onedoc/react-print";

const onedoc = new Onedoc(process.env.ONEDOC_API_KEY!);

(async () => {
  const data: InvoiceProps = {
    // your document's content is now editable in a an object
    id: 1029,
    seller: {
      name: "Onedoc, Inc,",
      address: "1600 Pennsylvania Avenue NW,",
      city: "Washington",
      ZIPCode: "DC 20500",
      country: "United States of America",
    },
    buyer: {
      name: "Titouan Launay",
      address: "172 Foxcol",
      city: "Gotham City,",
      ZIPCode: "NJ 12345",
      country: "United States of America",
    },
    date: new Date(),
    dateString: new Intl.DateTimeFormat("en-US", {
      year: "numeric",
      month: "long",
      day: "numeric",
    }).format(new Date()),
    items: [
      {
        name: "Onedoc Startup Plan",
        price: 100,
        quantity: 1,
        total: 100,
      },
      {
        name: "Onedoc Document Hosting",
        price: 0,
        quantity: 3462,
        total: 0,
      },
    ],
  };
  const html = await compile(<Invoice {...data} />);

  const { file } = await onedoc.render({
    html,
    test: false,
    save: false,
    assets: [],
  });

  fs.writeFileSync("./invoice.pdf", Buffer.from(file));
})();
Enter fullscreen mode Exit fullscreen mode

And this is what you get:

Image description

Join the movement

We are building a community of developers and designers to make documents great again. We believe that documents should be as dynamic and beautiful as the web. We are looking for feedback, contributors, and early adopters.

If you are interested, you can:

We are excited to see what you will build with Onedoc.

This article was taken from Onedoc's blog

Top comments (1)

Collapse
 
kigazon profile image
Josef Held

Interesting. I have a couple questions though:
Onedoc is a paid service right?
How do they handle GDPR?
Where are their servers located? Outside of the EU?
What prerequisites are there to using it?