feat: add email message field to invoices and update related components

This commit is contained in:
2026-04-28 01:06:45 -04:00
parent 4108019eab
commit 915ec103fc
16 changed files with 361 additions and 356 deletions
+15 -2
View File
@@ -7,6 +7,16 @@ import { env } from "~/env";
import { generateInvoicePDFBlob } from "~/lib/pdf-export";
import { generateInvoiceEmailTemplate } from "~/lib/email-templates";
function plainTextToHtml(value: string) {
return value
.replace(/&/g, "&")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;")
.replace(/\n/g, "<br>");
}
export const emailRouter = createTRPCRouter({
sendInvoice: protectedProcedure
.input(
@@ -106,7 +116,6 @@ export const emailRouter = createTRPCRouter({
totalAmount: invoice.totalAmount,
taxRate: invoice.taxRate,
currency: invoice.currency,
notes: invoice.notes,
client: {
name: invoice.client.name,
email: invoice.client.email,
@@ -115,7 +124,11 @@ export const emailRouter = createTRPCRouter({
items: invoice.items,
},
customContent: input.customContent,
customMessage: input.customMessage,
customMessage:
input.customMessage ??
(invoice.emailMessage
? plainTextToHtml(invoice.emailMessage)
: undefined),
userName,
userEmail,
baseUrl: process.env.NEXT_PUBLIC_APP_URL ?? "http://localhost:3000",
+11
View File
@@ -29,6 +29,7 @@ const createInvoiceSchema = z.object({
dueDate: z.date(),
status: z.enum(["draft", "sent", "paid"]).default("draft"),
notes: z.string().optional().or(z.literal("")),
emailMessage: z.string().optional().or(z.literal("")),
taxRate: z.number().min(0).max(100).default(0),
currency: z.string().length(3).default("USD"),
items: z.array(invoiceItemSchema).min(1, "At least one item is required"),
@@ -156,6 +157,8 @@ export const invoicesRouter = createTRPCRouter({
? null
: invoiceData.businessId,
notes: invoiceData.notes === "" ? null : invoiceData.notes,
emailMessage:
invoiceData.emailMessage === "" ? null : invoiceData.emailMessage,
};
// Verify business exists and belongs to user (if provided)
@@ -263,6 +266,14 @@ export const invoicesRouter = createTRPCRouter({
...(invoiceData.notes !== undefined
? { notes: invoiceData.notes === "" ? null : invoiceData.notes }
: {}),
...(invoiceData.emailMessage !== undefined
? {
emailMessage:
invoiceData.emailMessage === ""
? null
: invoiceData.emailMessage,
}
: {}),
};
// Verify invoice exists and belongs to user
+3
View File
@@ -94,6 +94,7 @@ const InvoiceBackupSchema = z.object({
totalAmount: z.number().default(0),
taxRate: z.number().default(0),
notes: z.string().optional(),
emailMessage: z.string().optional(),
items: z.array(InvoiceItemBackupSchema),
});
@@ -562,6 +563,7 @@ export const settingsRouter = createTRPCRouter({
totalAmount: invoice.totalAmount,
taxRate: invoice.taxRate,
notes: invoice.notes ?? undefined,
emailMessage: invoice.emailMessage ?? undefined,
items: invoice.items,
})),
};
@@ -641,6 +643,7 @@ export const settingsRouter = createTRPCRouter({
totalAmount: invoiceData.totalAmount,
taxRate: invoiceData.taxRate,
notes: invoiceData.notes,
emailMessage: invoiceData.emailMessage,
createdById: userId,
})
.returning({ id: invoices.id });
+3
View File
@@ -237,6 +237,9 @@ async function isMigrationApplied(client: Pool, tag: string): Promise<boolean> {
"pdfTemplate",
);
}
if (tag === "0007_invoice_email_message") {
return columnExists(client, "public", "beenvoice_invoice", "emailMessage");
}
// Unknown migration — assume not applied so it runs
return false;
}
+1
View File
@@ -320,6 +320,7 @@ export const invoices = createTable(
totalAmount: d.real().notNull().default(0),
taxRate: d.real().notNull().default(0.0),
notes: d.varchar({ length: 1000 }),
emailMessage: d.varchar({ length: 2000 }),
currency: d.varchar({ length: 3 }).default("USD").notNull(),
createdById: d
.varchar({ length: 255 })