add invoice prefixes, currency passing to pdf gen

This commit is contained in:
2026-04-10 01:28:14 -04:00
parent af392e1bc9
commit 4214a4b4de
6 changed files with 206 additions and 84 deletions
@@ -39,7 +39,23 @@ export function PDFDownloadButton({
throw new Error("Invoice not found"); throw new Error("Invoice not found");
} }
await generateInvoicePDF(invoiceData); // Map invoice to PDF format with currency support
const pdfData = {
invoiceNumber: invoiceData.invoiceNumber,
invoicePrefix: invoiceData.invoicePrefix,
issueDate: new Date(invoiceData.issueDate),
dueDate: new Date(invoiceData.dueDate),
status: invoiceData.status,
totalAmount: invoiceData.totalAmount,
taxRate: invoiceData.taxRate,
currency: invoiceData.currency ?? "USD",
notes: invoiceData.notes,
business: invoiceData.business,
client: invoiceData.client,
items: invoiceData.items,
};
await generateInvoicePDF(pdfData);
toast.success("PDF downloaded successfully"); toast.success("PDF downloaded successfully");
} catch (error) { } catch (error) {
console.error("PDF generation error:", error); console.error("PDF generation error:", error);
+17
View File
@@ -15,6 +15,7 @@ import {
SelectValue, SelectValue,
} from "~/components/ui/select"; } from "~/components/ui/select";
import { DatePicker } from "~/components/ui/date-picker"; import { DatePicker } from "~/components/ui/date-picker";
import { Input } from "~/components/ui/input";
import { NumberInput } from "~/components/ui/number-input"; import { NumberInput } from "~/components/ui/number-input";
import { PageHeader } from "~/components/layout/page-header"; import { PageHeader } from "~/components/layout/page-header";
import { InvoiceLineItems } from "./invoice-line-items"; import { InvoiceLineItems } from "./invoice-line-items";
@@ -80,6 +81,7 @@ export default function InvoiceForm({ invoiceId }: InvoiceFormProps) {
// State // State
const [formData, setFormData] = useState<InvoiceFormData>({ const [formData, setFormData] = useState<InvoiceFormData>({
invoiceNumber: `INV-${new Date().toISOString().slice(0, 10).replace(/-/g, "")}-${Date.now().toString().slice(-6)}`, invoiceNumber: `INV-${new Date().toISOString().slice(0, 10).replace(/-/g, "")}-${Date.now().toString().slice(-6)}`,
invoicePrefix: "#",
businessId: "", businessId: "",
clientId: "", clientId: "",
issueDate: new Date(), issueDate: new Date(),
@@ -150,6 +152,7 @@ export default function InvoiceForm({ invoiceId }: InvoiceFormProps) {
) || []; ) || [];
setFormData({ setFormData({
invoiceNumber: existingInvoice.invoiceNumber, invoiceNumber: existingInvoice.invoiceNumber,
invoicePrefix: existingInvoice.invoicePrefix ?? "#",
businessId: existingInvoice.businessId ?? "", businessId: existingInvoice.businessId ?? "",
clientId: existingInvoice.clientId, clientId: existingInvoice.clientId,
issueDate: new Date(existingInvoice.issueDate), issueDate: new Date(existingInvoice.issueDate),
@@ -317,6 +320,7 @@ export default function InvoiceForm({ invoiceId }: InvoiceFormProps) {
try { try {
const payload = { const payload = {
invoiceNumber: formData.invoiceNumber, invoiceNumber: formData.invoiceNumber,
invoicePrefix: formData.invoicePrefix,
businessId: formData.businessId || "", businessId: formData.businessId || "",
clientId: formData.clientId, clientId: formData.clientId,
issueDate: formData.issueDate, issueDate: formData.issueDate,
@@ -520,6 +524,19 @@ export default function InvoiceForm({ invoiceId }: InvoiceFormProps) {
/> />
</div> </div>
</div> </div>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-3 sm:gap-4">
<div className="space-y-2">
<Label>Prefix</Label>
<Input
value={formData.invoicePrefix}
onChange={(e) =>
updateField("invoicePrefix", e.target.value)
}
placeholder="#"
className="w-full"
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div className="space-y-2"> <div className="space-y-2">
<Label>Tax Rate</Label> <Label>Tax Rate</Label>
+1
View File
@@ -14,6 +14,7 @@ export interface InvoiceItem {
export interface InvoiceFormData { export interface InvoiceFormData {
invoiceNumber: string; invoiceNumber: string;
invoicePrefix: string;
businessId: string; businessId: string;
clientId: string; clientId: string;
issueDate: Date; issueDate: Date;
+68 -38
View File
@@ -5,11 +5,26 @@ import {
View, View,
Image, Image,
StyleSheet, StyleSheet,
Font,
pdf, pdf,
} from "@react-pdf/renderer"; } from "@react-pdf/renderer";
import { saveAs } from "file-saver"; import { saveAs } from "file-saver";
import React from "react"; import React from "react";
Font.register({
family: "Frutiger",
fonts: [
{
src: "/fonts/frutiger/Frutiger.ttf",
fontWeight: "normal",
},
{
src: "/fonts/frutiger/Frutiger_bold.ttf",
fontWeight: "bold",
},
],
});
// Fallback download function for better browser compatibility // Fallback download function for better browser compatibility
function downloadBlob(blob: Blob, filename: string): void { function downloadBlob(blob: Blob, filename: string): void {
try { try {
@@ -56,11 +71,13 @@ function downloadBlob(blob: Blob, filename: string): void {
interface InvoiceData { interface InvoiceData {
invoiceNumber: string; invoiceNumber: string;
invoicePrefix?: string | null;
issueDate: Date; issueDate: Date;
dueDate: Date; dueDate: Date;
status: string; status: string;
totalAmount: number; totalAmount: number;
taxRate: number; taxRate: number;
currency?: string | null;
notes?: string | null; notes?: string | null;
business?: { business?: {
name: string; name: string;
@@ -100,7 +117,7 @@ const styles = StyleSheet.create({
page: { page: {
flexDirection: "column", flexDirection: "column",
backgroundColor: "#ffffff", backgroundColor: "#ffffff",
fontFamily: "Helvetica", fontFamily: "Frutiger",
fontSize: 10, fontSize: 10,
paddingTop: 40, paddingTop: 40,
paddingBottom: 80, paddingBottom: 80,
@@ -127,7 +144,7 @@ const styles = StyleSheet.create({
}, },
businessName: { businessName: {
fontFamily: "Helvetica-Bold", fontFamily: "Frutiger-Bold",
fontSize: 18, fontSize: 18,
color: "#0f0f0f", color: "#0f0f0f",
marginBottom: 4, marginBottom: 4,
@@ -135,7 +152,7 @@ const styles = StyleSheet.create({
businessInfo: { businessInfo: {
fontSize: 10, fontSize: 10,
fontFamily: "Helvetica", fontFamily: "Frutiger",
color: "#6b7280", color: "#6b7280",
lineHeight: 1.4, lineHeight: 1.4,
marginBottom: 3, marginBottom: 3,
@@ -143,7 +160,7 @@ const styles = StyleSheet.create({
businessAddress: { businessAddress: {
fontSize: 10, fontSize: 10,
fontFamily: "Helvetica", fontFamily: "Frutiger",
color: "#6b7280", color: "#6b7280",
lineHeight: 1.4, lineHeight: 1.4,
marginTop: 4, marginTop: 4,
@@ -156,14 +173,14 @@ const styles = StyleSheet.create({
invoiceTitle: { invoiceTitle: {
fontSize: 28, fontSize: 28,
fontFamily: "Helvetica-Bold", fontFamily: "Frutiger-Bold",
color: "#0f0f0f", color: "#0f0f0f",
marginBottom: 8, marginBottom: 8,
}, },
invoiceNumber: { invoiceNumber: {
fontSize: 14, fontSize: 14,
fontFamily: "Helvetica-Bold", fontFamily: "Frutiger-Bold",
color: "#374151", color: "#374151",
marginBottom: 4, marginBottom: 4,
}, },
@@ -172,7 +189,7 @@ const styles = StyleSheet.create({
paddingHorizontal: 8, paddingHorizontal: 8,
paddingVertical: 4, paddingVertical: 4,
fontSize: 11, fontSize: 11,
fontFamily: "Helvetica-Bold", fontFamily: "Frutiger-Bold",
textAlign: "center", textAlign: "center",
}, },
@@ -200,13 +217,13 @@ const styles = StyleSheet.create({
sectionTitle: { sectionTitle: {
fontSize: 12, fontSize: 12,
fontFamily: "Helvetica-Bold", fontFamily: "Frutiger-Bold",
color: "#0f0f0f", color: "#0f0f0f",
marginBottom: 12, marginBottom: 12,
}, },
clientName: { clientName: {
fontFamily: "Helvetica-Bold", fontFamily: "Frutiger-Bold",
fontSize: 12, fontSize: 12,
color: "#0f0f0f", color: "#0f0f0f",
marginBottom: 2, marginBottom: 2,
@@ -214,7 +231,7 @@ const styles = StyleSheet.create({
clientInfo: { clientInfo: {
fontSize: 10, fontSize: 10,
fontFamily: "Helvetica", fontFamily: "Frutiger",
color: "#6b7280", color: "#6b7280",
lineHeight: 1.4, lineHeight: 1.4,
marginBottom: 2, marginBottom: 2,
@@ -222,7 +239,7 @@ const styles = StyleSheet.create({
clientAddress: { clientAddress: {
fontSize: 10, fontSize: 10,
fontFamily: "Helvetica", fontFamily: "Frutiger",
color: "#6b7280", color: "#6b7280",
lineHeight: 1.4, lineHeight: 1.4,
marginTop: 4, marginTop: 4,
@@ -236,14 +253,14 @@ const styles = StyleSheet.create({
detailLabel: { detailLabel: {
fontSize: 11, fontSize: 11,
fontFamily: "Helvetica", fontFamily: "Frutiger",
color: "#6b7280", color: "#6b7280",
flex: 1, flex: 1,
}, },
detailValue: { detailValue: {
fontSize: 10, fontSize: 10,
fontFamily: "Helvetica-Bold", fontFamily: "Frutiger-Bold",
color: "#0f0f0f", color: "#0f0f0f",
flex: 1, flex: 1,
textAlign: "right", textAlign: "right",
@@ -259,21 +276,21 @@ const styles = StyleSheet.create({
notesTitle: { notesTitle: {
fontSize: 11, fontSize: 11,
fontFamily: "Helvetica-Bold", fontFamily: "Frutiger-Bold",
color: "#0f0f0f", color: "#0f0f0f",
marginBottom: 6, marginBottom: 6,
}, },
notesContent: { notesContent: {
fontSize: 10, fontSize: 10,
fontFamily: "Helvetica", fontFamily: "Frutiger",
color: "#374151", color: "#374151",
lineHeight: 1.4, lineHeight: 1.4,
}, },
businessContact: { businessContact: {
fontSize: 9, fontSize: 9,
fontFamily: "Helvetica", fontFamily: "Frutiger",
color: "#6b7280", color: "#6b7280",
lineHeight: 1.4, lineHeight: 1.4,
}, },
@@ -297,7 +314,7 @@ const styles = StyleSheet.create({
abridgedBusinessName: { abridgedBusinessName: {
fontSize: 12, fontSize: 12,
fontFamily: "Helvetica-Bold", fontFamily: "Frutiger-Bold",
color: "#0f0f0f", color: "#0f0f0f",
}, },
@@ -309,13 +326,13 @@ const styles = StyleSheet.create({
abridgedInvoiceTitle: { abridgedInvoiceTitle: {
fontSize: 14, fontSize: 14,
fontFamily: "Helvetica-Bold", fontFamily: "Frutiger-Bold",
color: "#0f0f0f", color: "#0f0f0f",
}, },
abridgedInvoiceNumber: { abridgedInvoiceNumber: {
fontSize: 12, fontSize: 12,
fontFamily: "Helvetica-Bold", fontFamily: "Frutiger-Bold",
color: "#374151", color: "#374151",
}, },
@@ -335,7 +352,7 @@ const styles = StyleSheet.create({
tableHeaderCell: { tableHeaderCell: {
fontSize: 10, fontSize: 10,
fontFamily: "Helvetica-Bold", fontFamily: "Frutiger-Bold",
color: "#374151", color: "#374151",
paddingHorizontal: 4, paddingHorizontal: 4,
}, },
@@ -380,7 +397,7 @@ const styles = StyleSheet.create({
color: "#0f0f0f", color: "#0f0f0f",
paddingHorizontal: 4, paddingHorizontal: 4,
paddingVertical: 2, paddingVertical: 2,
fontFamily: "Helvetica", fontFamily: "Frutiger",
}, },
tableCellDate: { tableCellDate: {
@@ -396,7 +413,7 @@ const styles = StyleSheet.create({
paddingHorizontal: 2, paddingHorizontal: 2,
textAlign: "left", textAlign: "left",
flexWrap: "wrap", flexWrap: "wrap",
fontFamily: "Helvetica", fontFamily: "Frutiger",
}, },
tableCellHours: { tableCellHours: {
@@ -454,7 +471,7 @@ const styles = StyleSheet.create({
totalLabel: { totalLabel: {
fontSize: 11, fontSize: 11,
color: "#6b7280", color: "#6b7280",
fontFamily: "Helvetica", fontFamily: "Frutiger",
}, },
totalAmount: { totalAmount: {
@@ -472,7 +489,7 @@ const styles = StyleSheet.create({
finalTotalLabel: { finalTotalLabel: {
fontSize: 12, fontSize: 12,
fontFamily: "Helvetica-Bold", fontFamily: "Frutiger-Bold",
color: "#0f0f0f", color: "#0f0f0f",
}, },
@@ -484,7 +501,7 @@ const styles = StyleSheet.create({
itemCount: { itemCount: {
fontSize: 9, fontSize: 9,
fontFamily: "Helvetica", fontFamily: "Frutiger",
color: "#9ca3af", color: "#9ca3af",
textAlign: "center", textAlign: "center",
marginTop: 6, marginTop: 6,
@@ -511,16 +528,16 @@ const styles = StyleSheet.create({
pageNumber: { pageNumber: {
fontSize: 10, fontSize: 10,
fontFamily: "Helvetica", fontFamily: "Frutiger",
color: "#6b7280", color: "#6b7280",
}, },
}); });
// Helper functions // Helper functions
const formatCurrency = (amount: number) => { const formatCurrency = (amount: number, currency = "USD") => {
return new Intl.NumberFormat("en-US", { return new Intl.NumberFormat("en-US", {
style: "currency", style: "currency",
currency: "USD", currency,
}).format(amount); }).format(amount);
}; };
@@ -574,7 +591,7 @@ function estimateTextHeight(
): number { ): number {
if (!text) return fontSize * lineHeight; if (!text) return fontSize * lineHeight;
// Rough character width estimation for Helvetica at given font size // Rough character width estimation for Frutiger at given font size
const avgCharWidth = fontSize * 0.6; const avgCharWidth = fontSize * 0.6;
const maxCharsPerLine = Math.floor(maxWidth / avgCharWidth); const maxCharsPerLine = Math.floor(maxWidth / avgCharWidth);
@@ -807,7 +824,10 @@ const DenseHeader: React.FC<{ invoice: InvoiceData }> = ({ invoice }) => (
<View style={styles.invoiceSection}> <View style={styles.invoiceSection}>
<Text style={styles.invoiceTitle}>INVOICE</Text> <Text style={styles.invoiceTitle}>INVOICE</Text>
<Text style={styles.invoiceNumber}>#{invoice.invoiceNumber}</Text> <Text style={styles.invoiceNumber}>
{invoice.invoicePrefix ?? "#"}
{invoice.invoiceNumber}
</Text>
<View style={getStatusStyle(invoice.status)}> <View style={getStatusStyle(invoice.status)}>
<Text>{getStatusLabel(invoice.status)}</Text> <Text>{getStatusLabel(invoice.status)}</Text>
</View> </View>
@@ -873,7 +893,10 @@ const AbridgedHeader: React.FC<{ invoice: InvoiceData }> = ({ invoice }) => (
</Text> </Text>
<View style={styles.abridgedInvoiceInfo}> <View style={styles.abridgedInvoiceInfo}>
<Text style={styles.abridgedInvoiceTitle}>INVOICE</Text> <Text style={styles.abridgedInvoiceTitle}>INVOICE</Text>
<Text style={styles.abridgedInvoiceNumber}>#{invoice.invoiceNumber}</Text> <Text style={styles.abridgedInvoiceNumber}>
{invoice.invoicePrefix ?? "#"}
{invoice.invoiceNumber}
</Text>
</View> </View>
</View> </View>
); );
@@ -922,7 +945,7 @@ const Footer: React.FC = () => (
<Text <Text
style={{ style={{
fontSize: 9, fontSize: 9,
fontFamily: "Helvetica", fontFamily: "Frutiger",
color: "#6b7280", color: "#6b7280",
marginLeft: 8, marginLeft: 8,
}} }}
@@ -944,8 +967,10 @@ const TotalsSection: React.FC<{
invoice: InvoiceData; invoice: InvoiceData;
items: Array<NonNullable<InvoiceData["items"]>[0]>; items: Array<NonNullable<InvoiceData["items"]>[0]>;
}> = ({ invoice, items }) => { }> = ({ invoice, items }) => {
const currency = invoice.currency ?? "USD";
const subtotal = items.reduce((sum, item) => sum + (item?.amount ?? 0), 0); const subtotal = items.reduce((sum, item) => sum + (item?.amount ?? 0), 0);
const taxAmount = (subtotal * invoice.taxRate) / 100; const taxAmount = (subtotal * invoice.taxRate) / 100;
const total = subtotal + taxAmount;
return ( return (
<View style={styles.totalsContainer}> <View style={styles.totalsContainer}>
@@ -953,7 +978,7 @@ const TotalsSection: React.FC<{
<Text <Text
style={{ style={{
fontSize: 11, fontSize: 11,
fontFamily: "Helvetica-Bold", fontFamily: "Frutiger-Bold",
color: "#0f0f0f", color: "#0f0f0f",
textAlign: "center", textAlign: "center",
marginBottom: 8, marginBottom: 8,
@@ -965,20 +990,24 @@ const TotalsSection: React.FC<{
<View style={styles.totalRow}> <View style={styles.totalRow}>
<Text style={styles.totalLabel}>Subtotal:</Text> <Text style={styles.totalLabel}>Subtotal:</Text>
<Text style={styles.totalAmount}>{formatCurrency(subtotal)}</Text> <Text style={styles.totalAmount}>
{formatCurrency(subtotal, currency)}
</Text>
</View> </View>
{invoice.taxRate > 0 && ( {invoice.taxRate > 0 && (
<View style={styles.totalRow}> <View style={styles.totalRow}>
<Text style={styles.totalLabel}>Tax ({invoice.taxRate}%):</Text> <Text style={styles.totalLabel}>Tax ({invoice.taxRate}%):</Text>
<Text style={styles.totalAmount}>{formatCurrency(taxAmount)}</Text> <Text style={styles.totalAmount}>
{formatCurrency(taxAmount, currency)}
</Text>
</View> </View>
)} )}
<View style={styles.finalTotalRow}> <View style={styles.finalTotalRow}>
<Text style={styles.finalTotalLabel}>TOTAL:</Text> <Text style={styles.finalTotalLabel}>TOTAL:</Text>
<Text style={styles.finalTotalAmount}> <Text style={styles.finalTotalAmount}>
{formatCurrency(invoice.totalAmount)} {formatCurrency(total, currency)}
</Text> </Text>
</View> </View>
@@ -994,6 +1023,7 @@ const TotalsSection: React.FC<{
const InvoicePDF: React.FC<{ invoice: InvoiceData }> = ({ invoice }) => { const InvoicePDF: React.FC<{ invoice: InvoiceData }> = ({ invoice }) => {
const items = invoice.items?.filter(Boolean) ?? []; const items = invoice.items?.filter(Boolean) ?? [];
const paginatedItems = paginateItems(items, Boolean(invoice.notes)); const paginatedItems = paginateItems(items, Boolean(invoice.notes));
const currency = invoice.currency ?? "USD";
return ( return (
<Document> <Document>
@@ -1040,12 +1070,12 @@ const InvoicePDF: React.FC<{ invoice: InvoiceData }> = ({ invoice }) => {
{item.hours} {item.hours}
</Text> </Text>
<Text style={[styles.tableCell, styles.tableCellRate]}> <Text style={[styles.tableCell, styles.tableCellRate]}>
{formatCurrency(item.rate)} {formatCurrency(item.rate, currency)}
</Text> </Text>
<Text <Text
style={[styles.tableCell, styles.tableCellAmount]} style={[styles.tableCell, styles.tableCellAmount]}
> >
{formatCurrency(item.amount)} {formatCurrency(item.amount, currency)}
</Text> </Text>
</View> </View>
), ),
+30 -8
View File
@@ -18,6 +18,7 @@ const invoiceItemSchema = z.object({
const createInvoiceSchema = z.object({ const createInvoiceSchema = z.object({
invoiceNumber: z.string().min(1, "Invoice number is required"), invoiceNumber: z.string().min(1, "Invoice number is required"),
invoicePrefix: z.string().optional().default("#"),
businessId: z businessId: z
.string() .string()
.min(1, "Business is required") .min(1, "Business is required")
@@ -416,11 +417,17 @@ export const invoicesRouter = createTRPCRouter({
}); });
if (!invoice) { if (!invoice) {
throw new TRPCError({ code: "NOT_FOUND", message: "Invoice not found" }); throw new TRPCError({
code: "NOT_FOUND",
message: "Invoice not found",
});
} }
if (invoice.createdById !== ctx.session.user.id) { if (invoice.createdById !== ctx.session.user.id) {
throw new TRPCError({ code: "FORBIDDEN", message: "You don't have permission to update this invoice" }); throw new TRPCError({
code: "FORBIDDEN",
message: "You don't have permission to update this invoice",
});
} }
await ctx.db await ctx.db
@@ -428,18 +435,27 @@ export const invoicesRouter = createTRPCRouter({
.set({ status: input.status, updatedAt: new Date() }) .set({ status: input.status, updatedAt: new Date() })
.where(eq(invoices.id, input.id)); .where(eq(invoices.id, input.id));
return { success: true, message: `Invoice status updated to ${input.status}` }; return {
success: true,
message: `Invoice status updated to ${input.status}`,
};
} catch (error) { } catch (error) {
if (error instanceof TRPCError) throw error; if (error instanceof TRPCError) throw error;
throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "Failed to update invoice status", cause: error }); throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to update invoice status",
cause: error,
});
} }
}), }),
bulkUpdateStatus: protectedProcedure bulkUpdateStatus: protectedProcedure
.input(z.object({ .input(
z.object({
ids: z.array(z.string()).min(1), ids: z.array(z.string()).min(1),
status: z.enum(["draft", "sent", "paid"]), status: z.enum(["draft", "sent", "paid"]),
})) }),
)
.mutation(async ({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
// Only update invoices owned by this user // Only update invoices owned by this user
const owned = await ctx.db.query.invoices.findMany({ const owned = await ctx.db.query.invoices.findMany({
@@ -452,7 +468,10 @@ export const invoicesRouter = createTRPCRouter({
.map((inv) => inv.id); .map((inv) => inv.id);
if (ownedIds.length === 0) { if (ownedIds.length === 0) {
throw new TRPCError({ code: "NOT_FOUND", message: "No matching invoices found" }); throw new TRPCError({
code: "NOT_FOUND",
message: "No matching invoices found",
});
} }
await ctx.db await ctx.db
@@ -476,7 +495,10 @@ export const invoicesRouter = createTRPCRouter({
.map((inv) => inv.id); .map((inv) => inv.id);
if (ownedIds.length === 0) { if (ownedIds.length === 0) {
throw new TRPCError({ code: "NOT_FOUND", message: "No matching invoices found" }); throw new TRPCError({
code: "NOT_FOUND",
message: "No matching invoices found",
});
} }
await ctx.db.delete(invoices).where(inArray(invoices.id, ownedIds)); await ctx.db.delete(invoices).where(inArray(invoices.id, ownedIds));
+51 -15
View File
@@ -1,7 +1,6 @@
import { relations, sql } from "drizzle-orm"; import { relations, sql } from "drizzle-orm";
import { index, pgTableCreator } from "drizzle-orm/pg-core"; import { index, pgTableCreator } from "drizzle-orm/pg-core";
/** /**
* This is an example of how to use the multi-project schema feature of Drizzle ORM. Use the same * This is an example of how to use the multi-project schema feature of Drizzle ORM. Use the same
* database instance for multiple projects. * database instance for multiple projects.
@@ -22,7 +21,11 @@ export const users = createTable("user", (d) => ({
emailVerified: d.boolean().default(false).notNull(), emailVerified: d.boolean().default(false).notNull(),
image: d.varchar({ length: 255 }), image: d.varchar({ length: 255 }),
createdAt: d.timestamp().notNull().defaultNow(), createdAt: d.timestamp().notNull().defaultNow(),
updatedAt: d.timestamp().notNull().defaultNow().$onUpdate(() => new Date()), updatedAt: d
.timestamp()
.notNull()
.defaultNow()
.$onUpdate(() => new Date()),
password: d.varchar({ length: 255 }), // Matched DB: varchar(255) password: d.varchar({ length: 255 }), // Matched DB: varchar(255)
resetToken: d.varchar({ length: 255 }), // Matched DB: varchar(255) resetToken: d.varchar({ length: 255 }), // Matched DB: varchar(255)
resetTokenExpiry: d.timestamp(), resetTokenExpiry: d.timestamp(),
@@ -47,7 +50,11 @@ export const usersRelations = relations(users, ({ many }) => ({
export const accounts = createTable( export const accounts = createTable(
"account", "account",
(d) => ({ (d) => ({
id: d.text().notNull().primaryKey().$defaultFn(() => crypto.randomUUID()), // Matched DB: text id: d
.text()
.notNull()
.primaryKey()
.$defaultFn(() => crypto.randomUUID()), // Matched DB: text
userId: d userId: d
.varchar({ length: 255 }) .varchar({ length: 255 })
.notNull() .notNull()
@@ -62,11 +69,13 @@ export const accounts = createTable(
idToken: d.text(), idToken: d.text(),
password: d.text(), // Matched DB: text password: d.text(), // Matched DB: text
createdAt: d.timestamp().notNull().defaultNow(), createdAt: d.timestamp().notNull().defaultNow(),
updatedAt: d.timestamp().notNull().defaultNow().$onUpdate(() => new Date()), updatedAt: d
.timestamp()
.notNull()
.defaultNow()
.$onUpdate(() => new Date()),
}), }),
(t) => [ (t) => [index("account_userId_idx").on(t.userId)],
index("account_userId_idx").on(t.userId),
],
); );
export const accountsRelations = relations(accounts, ({ one }) => ({ export const accountsRelations = relations(accounts, ({ one }) => ({
@@ -76,7 +85,11 @@ export const accountsRelations = relations(accounts, ({ one }) => ({
export const sessions = createTable( export const sessions = createTable(
"session", "session",
(d) => ({ (d) => ({
id: d.text().notNull().primaryKey().$defaultFn(() => crypto.randomUUID()), // Matched DB: text id: d
.text()
.notNull()
.primaryKey()
.$defaultFn(() => crypto.randomUUID()), // Matched DB: text
userId: d userId: d
.varchar({ length: 255 }) .varchar({ length: 255 })
.notNull() .notNull()
@@ -86,7 +99,11 @@ export const sessions = createTable(
ipAddress: d.text(), // Matched DB: text ipAddress: d.text(), // Matched DB: text
userAgent: d.text(), // Matched DB: text userAgent: d.text(), // Matched DB: text
createdAt: d.timestamp().notNull().defaultNow(), createdAt: d.timestamp().notNull().defaultNow(),
updatedAt: d.timestamp().notNull().defaultNow().$onUpdate(() => new Date()), updatedAt: d
.timestamp()
.notNull()
.defaultNow()
.$onUpdate(() => new Date()),
}), }),
(t) => [index("session_userId_idx").on(t.userId)], (t) => [index("session_userId_idx").on(t.userId)],
); );
@@ -98,12 +115,20 @@ export const sessionsRelations = relations(sessions, ({ one }) => ({
export const verificationTokens = createTable( export const verificationTokens = createTable(
"verification_token", "verification_token",
(d) => ({ (d) => ({
id: d.text().notNull().primaryKey().$defaultFn(() => crypto.randomUUID()), // Matched DB: text id: d
.text()
.notNull()
.primaryKey()
.$defaultFn(() => crypto.randomUUID()), // Matched DB: text
identifier: d.varchar({ length: 255 }).notNull(), identifier: d.varchar({ length: 255 }).notNull(),
value: d.varchar({ length: 255 }).notNull(), value: d.varchar({ length: 255 }).notNull(),
expiresAt: d.timestamp().notNull(), expiresAt: d.timestamp().notNull(),
createdAt: d.timestamp().notNull().defaultNow(), createdAt: d.timestamp().notNull().defaultNow(),
updatedAt: d.timestamp().notNull().defaultNow().$onUpdate(() => new Date()), updatedAt: d
.timestamp()
.notNull()
.defaultNow()
.$onUpdate(() => new Date()),
}), }),
(t) => [index("verification_token_identifier_idx").on(t.identifier)], (t) => [index("verification_token_identifier_idx").on(t.identifier)],
); );
@@ -111,14 +136,25 @@ export const verificationTokens = createTable(
export const ssoProviders = createTable( export const ssoProviders = createTable(
"sso_provider", "sso_provider",
(d) => ({ (d) => ({
id: d.varchar({ length: 255 }).notNull().primaryKey().$defaultFn(() => crypto.randomUUID()), id: d
.varchar({ length: 255 })
.notNull()
.primaryKey()
.$defaultFn(() => crypto.randomUUID()),
providerId: d.varchar({ length: 255 }).notNull().unique(), providerId: d.varchar({ length: 255 }).notNull().unique(),
userId: d.varchar({ length: 255 }).notNull().references(() => users.id), userId: d
.varchar({ length: 255 })
.notNull()
.references(() => users.id),
redirectURI: d.varchar({ length: 255 }).notNull().default(""), // Added detailed fields redirectURI: d.varchar({ length: 255 }).notNull().default(""), // Added detailed fields
oidcConfig: d.text(), oidcConfig: d.text(),
samlConfig: d.text(), samlConfig: d.text(),
createdAt: d.timestamp().notNull().defaultNow(), createdAt: d.timestamp().notNull().defaultNow(),
updatedAt: d.timestamp().notNull().defaultNow().$onUpdate(() => new Date()), updatedAt: d
.timestamp()
.notNull()
.defaultNow()
.$onUpdate(() => new Date()),
}), }),
(t) => [index("sso_provider_user_id_idx").on(t.userId)], (t) => [index("sso_provider_user_id_idx").on(t.userId)],
); );
@@ -230,6 +266,7 @@ export const invoices = createTable(
.primaryKey() .primaryKey()
.$defaultFn(() => crypto.randomUUID()), .$defaultFn(() => crypto.randomUUID()),
invoiceNumber: d.varchar({ length: 100 }).notNull(), invoiceNumber: d.varchar({ length: 100 }).notNull(),
invoicePrefix: d.varchar({ length: 20 }).default("#"),
businessId: d.varchar({ length: 255 }).references(() => businesses.id), businessId: d.varchar({ length: 255 }).references(() => businesses.id),
clientId: d clientId: d
.varchar({ length: 255 }) .varchar({ length: 255 })
@@ -411,4 +448,3 @@ export const invoiceTemplatesRelations = relations(
}), }),
}), }),
); );