Creating a dynamic invoice

Creating a dynamic invoice

Monday, February 12, 2024

Pierre Dorge

Studied Computer Science. Co-founder @ Onedoc. I'm passionate about technology, politics, sports and food. I love learning.

Let’s dive further into Onedoc together.
This article showcases how you can leverage react and typescript to make invoice generation simple and convenient!

Follow these 3 simple steps in implementing your first dynamic invoice with Onedoc! 📄

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.
1
export interface Party {
2
name: string;
3
address: string;
4
city: string;
5
ZIPCode: string;
6
country: string;
7
}
8
9
export interface Item {
10
name: string;
11
description?: string;
12
price: number;
13
quantity: number;
14
total: number;
15
}
16
17
export interface InvoiceProps {
18
id: number;
19
buyer: Party;
20
seller: Party;
21
date: Date;
22
dateString: string;
23
items: Item[];
24
}

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.

1
import React from "react";
2
import { Party, Item } from "./dataModels";
3
4
export const Logo = (props) => {
5
return (
6
<svg
7
xmlns="http://www.w3.org/2000/svg"
8
x="0"
9
y="0"
10
enableBackground="new 0 0 46.15 9.31"
11
version="1.1"
12
viewBox="0 0 46.15 9.31"
13
xmlSpace="preserve"
14
fill="black"
15
{...props}
16
>
17
<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>
18
<path d="M1.49 8.09c.62.56 1.34.94 2.17 1.1v-2.8l-2.17 1.7z"></path>
19
</svg>
20
);
21
};
22
23
export const Right = ({ children }) => {
24
return <div className="text-right">{children}</div>;
25
};
26
27
export const InvoiceHeader = ({ props }) => {
28
return (
29
<div>
30
<h1 className="text-2xl font-bold">Invoice #{props.id}</h1>
31
<p className="text-xs">{props.dateString}</p>
32
</div>
33
);
34
};
35
36
export const LineSeparation = () => {
37
return <div className="h-px bg-gray-300 my-4" />;
38
};
39
40
export const PartySection = ({
41
party,
42
buyer,
43
}: {
44
party: Party;
45
buyer: boolean;
46
}) => {
47
let partyDetails = Object.entries(party).map(([key, value]) => (
48
<p className="p-0 mb-1">{value}</p>
49
));
50
51
return buyer ? (
52
<div>
53
<p className="p-0 mb-1">
54
<b>Bill to:</b>
55
</p>
56
{partyDetails}
57
</div>
58
) : (
59
<Right>{partyDetails}</Right>
60
);
61
};
62
63
export const ItemTable = ({ items }: { items: Item[] }) => {
64
const content = items.map((a) => {
65
return (
66
<React.Fragment key={items.indexOf(a)}>
67
<tr className="border-b border-gray-300">
68
<td className="py-2">{items.indexOf(a)}</td>
69
<td className="py-2">{a.name}</td>
70
<td className="py-2">${a.price}</td>
71
<td className="py-2">{a.quantity}</td>
72
<td className="py-2">${a.total}</td>
73
</tr>
74
</React.Fragment>
75
);
76
});
77
const totalPrice = items
78
.map((a) => {
79
return a.total;
80
})
81
.reduce((accumulator, currentValue) => {
82
return accumulator + currentValue;
83
}, 0);
84
return (
85
<table className="w-full my-12">
86
<tr className="border-b border-gray-300">
87
<th className="text-left font-bold py-2">Item</th>
88
<th className="text-left font-bold py-2">Description</th>
89
<th className="text-left font-bold py-2">Unit Price</th>
90
<th className="text-left font-bold py-2">Quantity</th>
91
<th className="text-left font-bold py-2">Amount</th>
92
</tr>
93
94
{content}
95
96
<tr className="border-b border-gray-300">
97
<th className="text-left font-bold py-2"></th>
98
<th className="text-left font-bold py-2">Total</th>
99
<th className="text-left font-bold py-2"></th>
100
<th className="text-left font-bold py-2"></th>
101
<th className="text-left font-bold py-2">${totalPrice}</th>
102
</tr>
103
</table>
104
);
105
};

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

1
import React from "react";
2
import { Footnote, PageBottom, Tailwind } from "@onedoc/react-print";
3
import {
4
PartySection,
5
ItemTable,
6
InvoiceHeader,
7
LineSeparation,
8
Logo,
9
} from "./components";
10
import { InvoiceProps } from "./dataModels";
11
12
export const Invoice = (props: InvoiceProps) => {
13
return (
14
<Tailwind>
15
<div className="font-[arial]">
16
<div className="flex justify-between items-end pb-4 mb-8">
17
<InvoiceHeader props={props} />
18
<Logo className="h-6" />
19
</div>
20
21
<PartySection party={props.seller} buyer={false} />
22
<LineSeparation />
23
24
<PartySection party={props.buyer} buyer={true} />
25
<LineSeparation />
26
27
<p className="p-0 leading-5">
28
All items below correspond to work completed in the month of{" "}
29
{props.date.toLocaleString("default", { month: "long" })}{" "}
30
{props.date.getFullYear()}. Payment is due within 15 days of receipt
31
of this invoice.
32
<Footnote>This includes non-business days.</Footnote>
33
</p>
34
35
<ItemTable items={props.items} />
36
37
<div className="bg-blue-100 p-3 rounded-md border-blue-300 text-blue-800 text-sm">
38
On {props.dateString}, Onedoc users will be upgraded free of charge to
39
our new cloud offering.
40
</div>
41
42
<PageBottom>
43
<LineSeparation />
44
<div className="text-gray-400 text-sm">Invoice #{props.id}</div>
45
</PageBottom>
46
</div>
47
</Tailwind>
48
);
49
};

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

1
import React from "react";
2
import fs from "fs";
3
import { Invoice } from "./invoice/invoice.tsx";
4
import { Onedoc } from "@onedoc/client";
5
import { InvoiceProps } from "./invoice/dataModels.tsx";
6
import { compile } from "@onedoc/react-print";
7
8
const onedoc = new Onedoc(process.env.ONEDOC_API_KEY!);
9
10
(async () => {
11
const data: InvoiceProps = {
12
// your document's content is now editable in a an object
13
id: 1029,
14
seller: {
15
name: "Onedoc, Inc,",
16
address: "1600 Pennsylvania Avenue NW,",
17
city: "Washington",
18
ZIPCode: "DC 20500",
19
country: "United States of America",
20
},
21
buyer: {
22
name: "Titouan Launay",
23
address: "172 Foxcol",
24
city: "Gotham City,",
25
ZIPCode: "NJ 12345",
26
country: "United States of America",
27
},
28
date: new Date(),
29
dateString: new Intl.DateTimeFormat("en-US", {
30
year: "numeric",
31
month: "long",
32
day: "numeric",
33
}).format(new Date()),
34
items: [
35
{
36
name: "Onedoc Startup Plan",
37
price: 100,
38
quantity: 1,
39
total: 100,
40
},
41
{
42
name: "Onedoc Document Hosting",
43
price: 0,
44
quantity: 3462,
45
total: 0,
46
},
47
],
48
};
49
const html = await compile(<Invoice {...data} />);
50
51
const { file } = await onedoc.render({
52
html,
53
test: false,
54
save: false,
55
assets: [],
56
});
57
58
fs.writeFileSync("./invoice.pdf", Buffer.from(file));
59
})();

Check it out in video !

Check this video out for a visual tutorial !

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.

Also on our blog