Skip to content

Commit

Permalink
Invoice blog
Browse files Browse the repository at this point in the history
  • Loading branch information
pontusab committed Oct 23, 2024
1 parent 7f039b9 commit b7aaeed
Show file tree
Hide file tree
Showing 8 changed files with 298 additions and 15 deletions.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion apps/website/src/app/updates/posts/apps.mdx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: "Introducing Apps"
publishedAt: "2024-10-29"
publishedAt: "2024-09-29"
summary: "We are excited to announce the launch of Apps in the Midday. You can now easily connect your favorite tools to streamline your workflow."
image: "/images/update/apps/apps.jpg"
tag: "Updates"
Expand Down
285 changes: 285 additions & 0 deletions apps/website/src/app/updates/posts/invoice-pdf.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,285 @@
---
title: "Invoice - generating PDFs"
publishedAt: "2024-10-23"
summary: "We are excited to announce the launch of Apps in the Midday. You can now easily connect your favorite tools to streamline your workflow."
image: "/images/update/invoice-pdf/pdf.jpg"
tag: "Updates"
---

With our upcoming Invoicing feature, we have explored different ways to generate PDF invoices, everything from running Pupper to using a headless browser on Cloudflare, to paid services and generating PDFs using React.

<br />
We noticed from the comunity that this is a common question on how to generate PDF invoices, so we decided to share our solution with you.
<br />


## Invoice in Midday
![PDF Invoices](/images/update/invoice-pdf/invoice.jpg)
<br />
We are building a new experience for invoices in Midday. You will be able to create and send invoices to your customers, and generate PDFs for each invoice.

<br />

Our interface is highly customizable with a visual editor where you can easily change the layout, add your logo, and customize the text to your liking.

<br />

We use an editor based on Tiptap to support rich text, AI genearation for grammar and improvin text with just one click.

<br />

While the editor saves the content using JSON, we also need a way to make this work with our PDF generation.

<br/>

When you have sent an invoice to a customer, they will recive a email with a unique link to the invoice. When they click on the link, it will render the invoice in a web page where you and the
customer can communicate in realtime using our chat interface.

<br />

You will also know if the customer has viewed the invoice and if they have any questions about the invoice.

<br />

## PDF Generation
![PDF Invoices](/images/update/invoice-pdf/pdf-invoice.jpg)
<br />

There are many ways to generate PDFs, and we have looked into many different solutions. We have also looked into paid services, but we wanted to make sure to give you full control over the invoices and not rely on another service.

<br />

We went with `react-pdf` to generate the PDFs. This is a great library that allows us to generate PDFs using React. We can easily customize the layout and add our own styles to the documents and it feels just like `react-email` concept where we use react to generate our templates.

<br />

The invoice is then generated and saved to your [Vault](https://midday.ai/vault), so we can match it to incoming transactions and mark it as paid.

<br />

We first create an API endpoint that will generate the PDF and return the PDF as Content Type `application/pdf`.

```tsx
import { InvoiceTemplate, renderToStream } from "@midday/invoice";
import { getInvoiceQuery } from "@midday/supabase/queries";
import { createClient } from "@midday/supabase/server";
import type { NextRequest } from "next/server";

export const preferredRegion = ["fra1", "sfo1", "iad1"];
export const dynamic = "force-dynamic";

export async function GET(req: NextRequest) {
const supabase = createClient();
const requestUrl = new URL(req.url);
const id = requestUrl.searchParams.get("id");
const size = requestUrl.searchParams.get("size") as "letter" | "a4";
const preview = requestUrl.searchParams.get("preview") === "true";

if (!id) {
return new Response("No invoice id provided", { status: 400 });
}

const { data } = await getInvoiceQuery(supabase, id);

if (!data) {
return new Response("Invoice not found", { status: 404 });
}

const stream = await renderToStream(await InvoiceTemplate({ ...data, size }));

const blob = await new Response(stream).blob();

const headers: Record<string, string> = {
"Content-Type": "application/pdf",
"Cache-Control": "no-store, max-age=0",
};

if (!preview) {
headers["Content-Disposition"] =
`attachment; filename="${data.invoice_number}.pdf"`;
}

return new Response(blob, { headers });
}
```

<br />

With this approach we can also add `?preview=true` to the URL to generate the PDF in the browser without downloading it. This is useful for previewing the invoice before generating the PDF.

## React PDF Invoice Template

And here is the template for the invoice, we register a custom font, generate a QR code and making sections and formatting the invoice.

<br />

You can find the full code for the invoice template [here](https://go.midday.ai/inv).



```tsx
import { Document, Font, Image, Page, Text, View } from "@react-pdf/renderer";
import QRCodeUtil from "qrcode";
import { EditorContent } from "../components/editor-content";
import { LineItems } from "../components/line-items";
import { Meta } from "../components/meta";
import { Note } from "../components/note";
import { PaymentDetails } from "../components/payment-details";
import { QRCode } from "../components/qr-code";
import { Summary } from "../components/summary";

const CDN_URL = "https://cdn.midday.ai";

Font.register({
family: "GeistMono",
fonts: [
{
src: `${CDN_URL}/fonts/GeistMono/ttf/GeistMono-Regular.ttf`,
fontWeight: 400,
},
{
src: `${CDN_URL}/fonts/GeistMono/ttf/GeistMono-Medium.ttf`,
fontWeight: 500,
},
],
});

export async function InvoiceTemplate({
invoice_number,
issue_date,
due_date,
template,
line_items,
customer_details,
from_details,
payment_details,
note_details,
currency,
vat,
tax,
amount,
size = "letter",
link,
}: Props) {
const qrCode = await QRCodeUtil.toDataURL(
link,
{
width: 40 * 3,
height: 40 * 3,
margin: 0,
},
);

return (
<Document>
<Page
size={size.toUpperCase() as "LETTER" | "A4"}
style={{
padding: 20,
backgroundColor: "#fff",
fontFamily: "GeistMono",
color: "#000",
}}
>
<View style={{ marginBottom: 20 }}>
{template?.logo_url && (
<Image
src={template.logo_url}
style={{
width: 78,
height: 78,
}}
/>
)}
</View>

<Meta
invoiceNoLabel={template.invoice_no_label}
issueDateLabel={template.issue_date_label}
dueDateLabel={template.due_date_label}
invoiceNo={invoice_number}
issueDate={issue_date}
dueDate={due_date}
/>

<View style={{ flexDirection: "row" }}>
<View style={{ flex: 1, marginRight: 10 }}>
<View style={{ marginBottom: 20 }}>
<Text style={{ fontSize: 9, fontWeight: 500 }}>
{template.from_label}
</Text>
<EditorContent content={from_details} />
</View>
</View>

<View style={{ flex: 1, marginLeft: 10 }}>
<View style={{ marginBottom: 20 }}>
<Text style={{ fontSize: 9, fontWeight: 500 }}>
{template.customer_label}
</Text>
<EditorContent content={customer_details} />
</View>
</View>
</View>

<LineItems
lineItems={line_items}
currency={currency}
descriptionLabel={template.description_label}
quantityLabel={template.quantity_label}
priceLabel={template.price_label}
totalLabel={template.total_label}
/>

<Summary
amount={amount}
tax={tax}
vat={vat}
currency={currency}
totalLabel={template.total_label}
taxLabel={template.tax_label}
vatLabel={template.vat_label}
/>

<View
style={{
flex: 1,
flexDirection: "column",
justifyContent: "flex-end",
}}
>
<View style={{ flexDirection: "row" }}>
<View style={{ flex: 1, marginRight: 10 }}>
<PaymentDetails
content={payment_details}
paymentLabel={template.payment_label}
/>

<QRCode data={qrCode} />
</View>

<View style={{ flex: 1, marginLeft: 10 }}>
<Note content={note_details} noteLabel={template.note_label} />
</View>
</View>
</View>
</Page>
</Document>
);
}
```

<br />
## What's next?
![PDF Invoices](/images/update/invoice-pdf/web-invoice.jpg)
<br />

We will be launching the Invoicing feature soon to our early access users, let us know if you want to be included in the early access program to get all the new features as soon as they are ready.

<br />

We would love to hear what you think about the Invoicing feature, and we would love to hear from you if you have any ideas or feedback for the feature.

<br />

[Sign up for an account](https://app.midday.ai) and start using Midday today.
2 changes: 1 addition & 1 deletion apps/website/src/app/updates/posts/slack-assistant.mdx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: "Building the Midday Slack Assistant"
publishedAt: "2024-10-29"
publishedAt: "2024-09-29"
summary: "This is a technical deep dive into how we built the Midday Slack Assistant using Vercel AI SDK, Trigger.dev and Supabase."
image: "/images/update/apps/slack.png"
tag: "Updates"
Expand Down
24 changes: 11 additions & 13 deletions apps/website/src/components/mdx.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@ interface TableProps {

function Table({ data }: TableProps) {
const headers = data.headers.map((header, index) => (
<th key={`header-${index}`}>{header}</th>
<th key={header}>{header}</th>
));

const rows = data.rows.map((row, rowIndex) => (
<tr key={`row-${rowIndex}`}>
<tr key={row.join("-")}>
{row.map((cell, cellIndex) => (
<td key={`cell-${rowIndex}-${cellIndex}`}>{cell}</td>
<td key={`${cell}-${cellIndex}`}>{cell}</td>
))}
</tr>
));
Expand Down Expand Up @@ -49,10 +49,10 @@ function CustomLink({ href, ...props }: CustomLinkProps) {
}

if (href.startsWith("#")) {
return <a {...props} />;
return <a href={href} {...props} />;
}

return <a target="_blank" rel="noopener noreferrer" {...props} />;
return <a href={href} target="_blank" rel="noopener noreferrer" {...props} />;
}

interface RoundedImageProps extends React.ComponentProps<typeof Image> {
Expand All @@ -65,7 +65,6 @@ function RoundedImage(props: RoundedImageProps) {

interface CodeProps {
children: string;
[key: string]: any;
}

function Code({ children, ...props }: CodeProps) {
Expand All @@ -77,11 +76,11 @@ function slugify(str: string): string {
return str
.toString()
.toLowerCase()
.trim() // Remove whitespace from both ends of a string
.replace(/\s+/g, "-") // Replace spaces with -
.replace(/&/g, "-and-") // Replace & with 'and'
.replace(/[^\w\-]+/g, "") // Remove all non-word characters except for -
.replace(/\-\-+/g, "-"); // Replace multiple - with single -
.trim()
.replace(/\s+/g, "-")
.replace(/&/g, "-and-")
.replace(/[^\w\-]+/g, "")
.replace(/\-\-+/g, "-");
}

function createHeading(level: number) {
Expand Down Expand Up @@ -130,8 +129,7 @@ const components = {
};

interface CustomMDXProps {
components?: Record<string, React.ComponentType<any>>;
[key: string]: any;
components?: Record<string, React.ComponentType<unknown>>;
}

export function CustomMDX(props: CustomMDXProps) {
Expand Down

0 comments on commit b7aaeed

Please sign in to comment.