| name | react-pdf-invoices |
| description | @react-pdf/renderer for generating Indian GST-compliant invoice PDFs in DropFlow. Use when building invoice templates, PDF generation in the worker, or any PDF output with GST breakdown tables. |
@react-pdf/renderer — GST Invoice PDFs
Package: @react-pdf/renderer
Location: apps/worker/src/pdf/
Installation
pnpm add @react-pdf/renderer --filter worker
Invoice PDF Template
import { Document, Page, View, Text, Image, StyleSheet, Font } from "@react-pdf/renderer";
Font.register({
family: "Noto Sans",
src: "https://fonts.gstatic.com/s/notosans/v30/o-0IIpQlx3QUlC5A4PNb4g.ttf",
});
const s = StyleSheet.create({
page: { padding: 40, fontFamily: "Noto Sans", fontSize: 9 },
header: { flexDirection: "row", justifyContent: "space-between", marginBottom: 20 },
title: { fontSize: 18, fontWeight: "bold" },
table: { width: "100%", marginTop: 10 },
tableRow: { flexDirection: "row", borderBottomWidth: 0.5, borderColor: "#ddd", paddingVertical: 4 },
tableHeader: { backgroundColor: "#f5f5f5", fontWeight: "bold" },
col1: { width: "5%" },
col2: { width: "30%" },
col3: { width: "10%", textAlign: "center" },
col4: { width: "10%", textAlign: "right" },
col5: { width: "15%", textAlign: "right" },
col6: { width: "15%", textAlign: "right" },
col7: { width: "15%", textAlign: "right" },
right: { textAlign: "right" },
bold: { fontWeight: "bold" },
section: { marginTop: 15 },
row: { flexDirection: "row", justifyContent: "space-between" },
divider: { borderBottomWidth: 1, borderColor: "#000", marginVertical: 8 },
});
const formatINR = (paise: number) =>
new Intl.NumberFormat("en-IN", { style: "currency", currency: "INR" }).format(paise / 100);
interface InvoicePDFProps {
invoice: {
invoiceNumber: string;
createdAt: string;
gstType: string;
subtotalPaise: number;
cgstPaise: number;
sgstPaise: number;
igstPaise: number;
totalTaxPaise: number;
totalPaise: number;
};
seller: { name: string; gstin: string; address: string; stateCode: string };
buyer: { name: string; address: string; gstin?: string; stateCode: string };
items: { name: string; hsn: string; qty: number; unitPaise: number; totalPaise: number; gstRate: number }[];
}
export function InvoicePDF({ invoice, seller, buyer, items }: InvoicePDFProps) {
return (
<Document>
<Page size="A4" style={s.page}>
{/* Header */}
<View style={s.header}>
<View>
<Text style={s.title}>TAX INVOICE</Text>
<Text>{invoice.invoiceNumber}</Text>
<Text>Date: {invoice.createdAt}</Text>
</View>
<View style={{ textAlign: "right" }}>
<Text style={s.bold}>{seller.name}</Text>
<Text>GSTIN: {seller.gstin}</Text>
<Text>{seller.address}</Text>
</View>
</View>
{/* Buyer Details */}
<View style={s.section}>
<Text =>Bill To:
{buyer.name}
{buyer.gstin && GSTIN: {buyer.gstin}}
{buyer.address}
{/* Items Table */}
#
Description
HSN
Qty
Unit Price
GST %
Total
{items.map((item, i) => (
{i + 1}
{item.name}
{item.hsn}
{item.qty}
{formatINR(item.unitPaise)}
{item.gstRate}%
{formatINR(item.totalPaise)}
))}
{/* Tax Summary */}
Subtotal
{formatINR(invoice.subtotalPaise)}
{invoice.gstType === "CGST_SGST" && (
CGST{formatINR(invoice.cgstPaise)}
SGST{formatINR(invoice.sgstPaise)}
)}
{invoice.gstType === "IGST" && (
IGST{formatINR(invoice.igstPaise)}
)}
{invoice.gstType === "EXPORT_LUT" && (
IGST (LUT){formatINR(0)}
)}
Total
{formatINR(invoice.totalPaise)}
</Document>
);
}
Rendering to Buffer (Worker)
import { renderToBuffer } from "@react-pdf/renderer";
import { put } from "@vercel/blob";
export async function generateInvoicePDF(invoiceData: InvoicePDFProps) {
const buffer = await renderToBuffer(<InvoicePDF {...invoiceData} />);
const { url } = await put(
`invoices/${invoiceData.invoice.invoiceNumber}.pdf`,
buffer,
{ access: "public" },
);
return url;
}
Invoice Number Format
INV/YYMM/NNNN
Sequential per tenant per financial year (April–March):
export async function generateInvoiceNumber(tenantId: string): Promise<string> {
const now = new Date();
const fy = now.getMonth() >= 3 ? now.getFullYear() : now.getFullYear() - 1;
const yymm = `${String(fy).slice(2)}${String(now.getMonth() + 1).padStart(2, "0")}`;
const count = await prisma.invoice.count({
where: { tenantId, invoiceNumber: { startsWith: `INV/${yymm}` } },
});
return `INV/${yymm}/${String(count + 1).padStart(4, "0")}`;
}
Conventions
- All money rendered with
en-IN Intl formatter (lakh separators)
- Include HSN code column — mandatory for GST invoices
- Show CGST+SGST or IGST based on
gstType — never both
- Upload PDF to Vercel Blob, store URL in
Invoice.pdfUrl
- Use Noto Sans font — supports ₹ symbol and Indian scripts