mirror of
https://github.com/soconnor0919/beenvoice.git
synced 2025-12-15 10:34:43 -05:00
Update pdf-export.tsx
This commit is contained in:
@@ -4,75 +4,11 @@ import {
|
|||||||
Text,
|
Text,
|
||||||
View,
|
View,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
Font,
|
|
||||||
Image,
|
|
||||||
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";
|
||||||
|
|
||||||
// Global font registration state
|
|
||||||
let fontsRegistered = false;
|
|
||||||
|
|
||||||
// Font registration helper that works in both client and server environments
|
|
||||||
const registerFonts = () => {
|
|
||||||
try {
|
|
||||||
// Avoid duplicate registration
|
|
||||||
if (fontsRegistered) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only register custom fonts on client side for now
|
|
||||||
// Server-side will use fallback fonts to avoid path/loading issues
|
|
||||||
if (typeof window !== "undefined") {
|
|
||||||
// Register Inter font
|
|
||||||
Font.register({
|
|
||||||
family: "Inter",
|
|
||||||
src: "/fonts/inter/Inter-Variable.ttf",
|
|
||||||
fontWeight: "normal",
|
|
||||||
});
|
|
||||||
|
|
||||||
Font.register({
|
|
||||||
family: "Inter",
|
|
||||||
src: "/fonts/inter/Inter-Italic-Variable.ttf",
|
|
||||||
fontStyle: "italic",
|
|
||||||
});
|
|
||||||
|
|
||||||
// Register Azeret Mono fonts for numbers and tables - multiple weights
|
|
||||||
Font.register({
|
|
||||||
family: "AzeretMono",
|
|
||||||
src: "/fonts/azeret/AzeretMono-Regular.ttf",
|
|
||||||
fontWeight: "normal",
|
|
||||||
});
|
|
||||||
|
|
||||||
Font.register({
|
|
||||||
family: "AzeretMono",
|
|
||||||
src: "/fonts/azeret/AzeretMono-Regular.ttf",
|
|
||||||
fontWeight: "semibold",
|
|
||||||
});
|
|
||||||
|
|
||||||
Font.register({
|
|
||||||
family: "AzeretMono",
|
|
||||||
src: "/fonts/azeret/AzeretMono-Regular.ttf",
|
|
||||||
fontWeight: "bold",
|
|
||||||
});
|
|
||||||
|
|
||||||
Font.register({
|
|
||||||
family: "AzeretMono",
|
|
||||||
src: "/fonts/azeret/AzeretMono-Italic-Variable.ttf",
|
|
||||||
fontStyle: "italic",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fontsRegistered = true;
|
|
||||||
} catch {
|
|
||||||
fontsRegistered = true; // Don't keep trying
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Register fonts immediately
|
|
||||||
registerFonts();
|
|
||||||
|
|
||||||
interface InvoiceData {
|
interface InvoiceData {
|
||||||
invoiceNumber: string;
|
invoiceNumber: string;
|
||||||
issueDate: Date;
|
issueDate: Date;
|
||||||
@@ -128,7 +64,7 @@ const styles = StyleSheet.create({
|
|||||||
// Dense header (first page)
|
// Dense header (first page)
|
||||||
denseHeader: {
|
denseHeader: {
|
||||||
marginBottom: 30,
|
marginBottom: 30,
|
||||||
borderBottom: "2px solid #10b981",
|
borderBottom: "1px solid #e5e7eb",
|
||||||
paddingBottom: 20,
|
paddingBottom: 20,
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -147,7 +83,7 @@ const styles = StyleSheet.create({
|
|||||||
businessName: {
|
businessName: {
|
||||||
fontFamily: "Helvetica-Bold",
|
fontFamily: "Helvetica-Bold",
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
color: "#111827",
|
color: "#0f0f0f",
|
||||||
marginBottom: 4,
|
marginBottom: 4,
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -155,12 +91,13 @@ const styles = StyleSheet.create({
|
|||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
fontFamily: "Helvetica",
|
fontFamily: "Helvetica",
|
||||||
color: "#6b7280",
|
color: "#6b7280",
|
||||||
lineHeight: 1.3,
|
lineHeight: 1.4,
|
||||||
marginBottom: 3,
|
marginBottom: 3,
|
||||||
},
|
},
|
||||||
|
|
||||||
businessAddress: {
|
businessAddress: {
|
||||||
fontSize: 11,
|
fontSize: 10,
|
||||||
|
fontFamily: "Helvetica",
|
||||||
color: "#6b7280",
|
color: "#6b7280",
|
||||||
lineHeight: 1.4,
|
lineHeight: 1.4,
|
||||||
marginTop: 4,
|
marginTop: 4,
|
||||||
@@ -172,36 +109,35 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
|
|
||||||
invoiceTitle: {
|
invoiceTitle: {
|
||||||
fontSize: 32,
|
fontSize: 28,
|
||||||
fontFamily: "Helvetica-Bold",
|
fontFamily: "Helvetica-Bold",
|
||||||
color: "#10b981",
|
color: "#0f0f0f",
|
||||||
marginBottom: 8,
|
marginBottom: 8,
|
||||||
},
|
},
|
||||||
|
|
||||||
invoiceNumber: {
|
invoiceNumber: {
|
||||||
fontSize: 15,
|
fontSize: 14,
|
||||||
fontFamily: "Courier-Bold",
|
fontFamily: "Helvetica-Bold",
|
||||||
color: "#111827",
|
color: "#374151",
|
||||||
marginBottom: 4,
|
marginBottom: 4,
|
||||||
},
|
},
|
||||||
|
|
||||||
statusBadge: {
|
statusBadge: {
|
||||||
paddingHorizontal: 12,
|
paddingHorizontal: 8,
|
||||||
paddingVertical: 4,
|
paddingVertical: 4,
|
||||||
borderRadius: 4,
|
fontSize: 11,
|
||||||
fontSize: 12,
|
|
||||||
fontFamily: "Helvetica-Bold",
|
fontFamily: "Helvetica-Bold",
|
||||||
textAlign: "center",
|
textAlign: "center",
|
||||||
},
|
},
|
||||||
|
|
||||||
statusPaid: {
|
statusPaid: {
|
||||||
backgroundColor: "#ecfdf5",
|
backgroundColor: "#f9fafb",
|
||||||
color: "#065f46",
|
color: "#374151",
|
||||||
},
|
},
|
||||||
|
|
||||||
statusUnpaid: {
|
statusUnpaid: {
|
||||||
backgroundColor: "#fef3c7",
|
backgroundColor: "#f9fafb",
|
||||||
color: "#92400e",
|
color: "#6b7280",
|
||||||
},
|
},
|
||||||
|
|
||||||
// Details section (first page only)
|
// Details section (first page only)
|
||||||
@@ -217,16 +153,16 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
|
|
||||||
sectionTitle: {
|
sectionTitle: {
|
||||||
fontSize: 14,
|
fontSize: 12,
|
||||||
fontFamily: "Helvetica-Bold",
|
fontFamily: "Helvetica-Bold",
|
||||||
color: "#111827",
|
color: "#0f0f0f",
|
||||||
marginBottom: 12,
|
marginBottom: 12,
|
||||||
},
|
},
|
||||||
|
|
||||||
clientName: {
|
clientName: {
|
||||||
fontFamily: "Helvetica-Bold",
|
fontFamily: "Helvetica-Bold",
|
||||||
fontSize: 14,
|
fontSize: 12,
|
||||||
color: "#111827",
|
color: "#0f0f0f",
|
||||||
marginBottom: 2,
|
marginBottom: 2,
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -234,12 +170,13 @@ const styles = StyleSheet.create({
|
|||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
fontFamily: "Helvetica",
|
fontFamily: "Helvetica",
|
||||||
color: "#6b7280",
|
color: "#6b7280",
|
||||||
lineHeight: 1.3,
|
lineHeight: 1.4,
|
||||||
marginBottom: 2,
|
marginBottom: 2,
|
||||||
},
|
},
|
||||||
|
|
||||||
clientAddress: {
|
clientAddress: {
|
||||||
fontSize: 11,
|
fontSize: 10,
|
||||||
|
fontFamily: "Helvetica",
|
||||||
color: "#6b7280",
|
color: "#6b7280",
|
||||||
lineHeight: 1.4,
|
lineHeight: 1.4,
|
||||||
marginTop: 4,
|
marginTop: 4,
|
||||||
@@ -253,14 +190,15 @@ const styles = StyleSheet.create({
|
|||||||
|
|
||||||
detailLabel: {
|
detailLabel: {
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
|
fontFamily: "Helvetica",
|
||||||
color: "#6b7280",
|
color: "#6b7280",
|
||||||
flex: 1,
|
flex: 1,
|
||||||
},
|
},
|
||||||
|
|
||||||
detailValue: {
|
detailValue: {
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
fontFamily: "Courier-Bold",
|
fontFamily: "Helvetica-Bold",
|
||||||
color: "#111827",
|
color: "#0f0f0f",
|
||||||
flex: 1,
|
flex: 1,
|
||||||
textAlign: "right",
|
textAlign: "right",
|
||||||
},
|
},
|
||||||
@@ -269,16 +207,14 @@ const styles = StyleSheet.create({
|
|||||||
notesSection: {
|
notesSection: {
|
||||||
marginTop: 0,
|
marginTop: 0,
|
||||||
marginBottom: 0,
|
marginBottom: 0,
|
||||||
padding: 15,
|
padding: 12,
|
||||||
backgroundColor: "#f9fafb",
|
backgroundColor: "#f9fafb",
|
||||||
borderRadius: 4,
|
|
||||||
border: "1px solid #e5e7eb",
|
|
||||||
},
|
},
|
||||||
|
|
||||||
notesTitle: {
|
notesTitle: {
|
||||||
fontSize: 12,
|
fontSize: 11,
|
||||||
fontFamily: "Helvetica-Bold",
|
fontFamily: "Helvetica-Bold",
|
||||||
color: "#111827",
|
color: "#0f0f0f",
|
||||||
marginBottom: 6,
|
marginBottom: 6,
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -293,7 +229,7 @@ const styles = StyleSheet.create({
|
|||||||
fontSize: 9,
|
fontSize: 9,
|
||||||
fontFamily: "Helvetica",
|
fontFamily: "Helvetica",
|
||||||
color: "#6b7280",
|
color: "#6b7280",
|
||||||
lineHeight: 1.2,
|
lineHeight: 1.4,
|
||||||
},
|
},
|
||||||
|
|
||||||
// Separator styles
|
// Separator styles
|
||||||
@@ -309,32 +245,32 @@ const styles = StyleSheet.create({
|
|||||||
justifyContent: "space-between",
|
justifyContent: "space-between",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
marginBottom: 20,
|
marginBottom: 20,
|
||||||
paddingBottom: 15,
|
paddingBottom: 12,
|
||||||
borderBottom: "1px solid #e5e7eb",
|
borderBottom: "1px solid #e5e7eb",
|
||||||
},
|
},
|
||||||
|
|
||||||
abridgedBusinessName: {
|
abridgedBusinessName: {
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontFamily: "Helvetica-Bold",
|
fontFamily: "Helvetica-Bold",
|
||||||
color: "#111827",
|
color: "#0f0f0f",
|
||||||
},
|
},
|
||||||
|
|
||||||
abridgedInvoiceInfo: {
|
abridgedInvoiceInfo: {
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
gap: 15,
|
gap: 12,
|
||||||
},
|
},
|
||||||
|
|
||||||
abridgedInvoiceTitle: {
|
abridgedInvoiceTitle: {
|
||||||
fontSize: 16,
|
fontSize: 14,
|
||||||
fontFamily: "Helvetica-Bold",
|
fontFamily: "Helvetica-Bold",
|
||||||
color: "#10b981",
|
color: "#0f0f0f",
|
||||||
},
|
},
|
||||||
|
|
||||||
abridgedInvoiceNumber: {
|
abridgedInvoiceNumber: {
|
||||||
fontSize: 13,
|
fontSize: 12,
|
||||||
fontFamily: "Courier-Bold",
|
fontFamily: "Helvetica-Bold",
|
||||||
color: "#111827",
|
color: "#374151",
|
||||||
},
|
},
|
||||||
|
|
||||||
// Table styles
|
// Table styles
|
||||||
@@ -345,16 +281,16 @@ const styles = StyleSheet.create({
|
|||||||
|
|
||||||
tableHeader: {
|
tableHeader: {
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
backgroundColor: "#f3f4f6",
|
backgroundColor: "#f9fafb",
|
||||||
borderBottom: "2px solid #10b981",
|
borderBottom: "1px solid #e5e7eb",
|
||||||
paddingVertical: 8,
|
paddingVertical: 8,
|
||||||
paddingHorizontal: 4,
|
paddingHorizontal: 4,
|
||||||
},
|
},
|
||||||
|
|
||||||
tableHeaderCell: {
|
tableHeaderCell: {
|
||||||
fontSize: 11,
|
fontSize: 10,
|
||||||
fontFamily: "Helvetica-Bold",
|
fontFamily: "Helvetica-Bold",
|
||||||
color: "#111827",
|
color: "#374151",
|
||||||
paddingHorizontal: 4,
|
paddingHorizontal: 4,
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -395,9 +331,10 @@ const styles = StyleSheet.create({
|
|||||||
|
|
||||||
tableCell: {
|
tableCell: {
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
color: "#111827",
|
color: "#0f0f0f",
|
||||||
paddingHorizontal: 4,
|
paddingHorizontal: 4,
|
||||||
paddingVertical: 2,
|
paddingVertical: 2,
|
||||||
|
fontFamily: "Helvetica",
|
||||||
},
|
},
|
||||||
|
|
||||||
tableCellDate: {
|
tableCellDate: {
|
||||||
@@ -413,6 +350,7 @@ const styles = StyleSheet.create({
|
|||||||
paddingHorizontal: 2,
|
paddingHorizontal: 2,
|
||||||
textAlign: "left",
|
textAlign: "left",
|
||||||
flexWrap: "wrap",
|
flexWrap: "wrap",
|
||||||
|
fontFamily: "Helvetica",
|
||||||
},
|
},
|
||||||
|
|
||||||
tableCellHours: {
|
tableCellHours: {
|
||||||
@@ -454,10 +392,8 @@ const styles = StyleSheet.create({
|
|||||||
|
|
||||||
totalsBox: {
|
totalsBox: {
|
||||||
width: "100%",
|
width: "100%",
|
||||||
padding: 10,
|
padding: 12,
|
||||||
backgroundColor: "#f9fafb",
|
backgroundColor: "#f9fafb",
|
||||||
border: "1px solid #e5e7eb",
|
|
||||||
borderRadius: 4,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
totalRow: {
|
totalRow: {
|
||||||
@@ -468,43 +404,42 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
|
|
||||||
totalLabel: {
|
totalLabel: {
|
||||||
fontSize: 12,
|
fontSize: 11,
|
||||||
color: "#475569",
|
color: "#6b7280",
|
||||||
fontFamily: "Helvetica",
|
fontFamily: "Helvetica",
|
||||||
},
|
},
|
||||||
|
|
||||||
totalAmount: {
|
totalAmount: {
|
||||||
fontSize: 12,
|
fontSize: 11,
|
||||||
fontFamily: "Courier-Bold",
|
fontFamily: "Courier-Bold",
|
||||||
color: "#1e293b",
|
color: "#0f0f0f",
|
||||||
},
|
},
|
||||||
|
|
||||||
finalTotalRow: {
|
finalTotalRow: {
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
justifyContent: "space-between",
|
justifyContent: "space-between",
|
||||||
marginTop: 6,
|
marginTop: 8,
|
||||||
paddingTop: 6,
|
paddingTop: 8,
|
||||||
},
|
},
|
||||||
|
|
||||||
finalTotalLabel: {
|
finalTotalLabel: {
|
||||||
fontSize: 14,
|
fontSize: 12,
|
||||||
fontFamily: "Helvetica-Bold",
|
fontFamily: "Helvetica-Bold",
|
||||||
color: "#1f2937",
|
color: "#0f0f0f",
|
||||||
},
|
},
|
||||||
|
|
||||||
finalTotalAmount: {
|
finalTotalAmount: {
|
||||||
fontSize: 15,
|
fontSize: 14,
|
||||||
fontFamily: "Courier-Bold",
|
fontFamily: "Courier-Bold",
|
||||||
color: "#10b981",
|
color: "#0f0f0f",
|
||||||
},
|
},
|
||||||
|
|
||||||
itemCount: {
|
itemCount: {
|
||||||
fontSize: 9,
|
fontSize: 9,
|
||||||
fontFamily: "Helvetica",
|
fontFamily: "Helvetica",
|
||||||
color: "#64748b",
|
color: "#9ca3af",
|
||||||
textAlign: "center",
|
textAlign: "center",
|
||||||
marginTop: 6,
|
marginTop: 6,
|
||||||
fontStyle: "italic",
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// Footer
|
// Footer
|
||||||
@@ -516,18 +451,19 @@ const styles = StyleSheet.create({
|
|||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
justifyContent: "space-between",
|
justifyContent: "space-between",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
paddingTop: 15,
|
paddingTop: 12,
|
||||||
borderTop: "1px solid #e5e7eb",
|
borderTop: "1px solid #e5e7eb",
|
||||||
},
|
},
|
||||||
|
|
||||||
footerLogo: {
|
footerLogo: {
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
alignItems: "center",
|
alignItems: "baseline",
|
||||||
gap: 8,
|
gap: 4,
|
||||||
},
|
},
|
||||||
|
|
||||||
pageNumber: {
|
pageNumber: {
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
|
fontFamily: "Helvetica",
|
||||||
color: "#6b7280",
|
color: "#6b7280",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -561,9 +497,21 @@ const getStatusLabel = (status: string) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getStatusStyle = (status: string) => {
|
const getStatusStyle = (status: string) => {
|
||||||
switch (status) {
|
switch (status.toLowerCase()) {
|
||||||
case "paid":
|
case "paid":
|
||||||
return [styles.statusBadge, styles.statusPaid];
|
return [styles.statusBadge, styles.statusPaid];
|
||||||
|
case "sent":
|
||||||
|
return [styles.statusBadge, styles.statusPaid];
|
||||||
|
case "overdue":
|
||||||
|
return [
|
||||||
|
styles.statusBadge,
|
||||||
|
{ backgroundColor: "#fef2f2", color: "#dc2626" },
|
||||||
|
];
|
||||||
|
case "draft":
|
||||||
|
return [
|
||||||
|
styles.statusBadge,
|
||||||
|
{ backgroundColor: "#f9fafb", color: "#9ca3af" },
|
||||||
|
];
|
||||||
default:
|
default:
|
||||||
return [styles.statusBadge, styles.statusUnpaid];
|
return [styles.statusBadge, styles.statusUnpaid];
|
||||||
}
|
}
|
||||||
@@ -914,13 +862,25 @@ const NotesSection: React.FC<{ invoice: InvoiceData }> = ({ invoice }) => {
|
|||||||
const Footer: React.FC = () => (
|
const Footer: React.FC = () => (
|
||||||
<View style={styles.footer} fixed>
|
<View style={styles.footer} fixed>
|
||||||
<View style={styles.footerLogo}>
|
<View style={styles.footerLogo}>
|
||||||
{/* eslint-disable-next-line jsx-a11y/alt-text */}
|
<Text
|
||||||
<Image
|
|
||||||
src="/beenvoice-logo.png"
|
|
||||||
style={{
|
style={{
|
||||||
height: 24,
|
fontSize: 12,
|
||||||
|
fontFamily: "Helvetica-Bold",
|
||||||
|
color: "#0f0f0f",
|
||||||
}}
|
}}
|
||||||
/>
|
>
|
||||||
|
beenvoice
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
fontSize: 9,
|
||||||
|
fontFamily: "Helvetica",
|
||||||
|
color: "#6b7280",
|
||||||
|
marginLeft: 8,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Professional Invoicing
|
||||||
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<Text
|
<Text
|
||||||
style={styles.pageNumber}
|
style={styles.pageNumber}
|
||||||
@@ -944,13 +904,12 @@ const TotalsSection: React.FC<{
|
|||||||
<View style={styles.totalsBox}>
|
<View style={styles.totalsBox}>
|
||||||
<Text
|
<Text
|
||||||
style={{
|
style={{
|
||||||
fontSize: 12,
|
fontSize: 11,
|
||||||
fontFamily: "Helvetica-Bold",
|
fontFamily: "Helvetica-Bold",
|
||||||
color: "#334155",
|
color: "#0f0f0f",
|
||||||
textAlign: "center",
|
textAlign: "center",
|
||||||
marginBottom: 6,
|
marginBottom: 8,
|
||||||
paddingBottom: 4,
|
paddingBottom: 6,
|
||||||
borderBottom: "1px solid #e2e8f0",
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
INVOICE SUMMARY
|
INVOICE SUMMARY
|
||||||
@@ -1066,9 +1025,6 @@ const InvoicePDF: React.FC<{ invoice: InvoiceData }> = ({ invoice }) => {
|
|||||||
// Export functions
|
// Export functions
|
||||||
export async function generateInvoicePDF(invoice: InvoiceData): Promise<void> {
|
export async function generateInvoicePDF(invoice: InvoiceData): Promise<void> {
|
||||||
try {
|
try {
|
||||||
// Ensure fonts are registered
|
|
||||||
registerFonts();
|
|
||||||
|
|
||||||
// Validate invoice data
|
// Validate invoice data
|
||||||
if (!invoice) {
|
if (!invoice) {
|
||||||
throw new Error("Invoice data is required");
|
throw new Error("Invoice data is required");
|
||||||
@@ -1082,13 +1038,8 @@ export async function generateInvoicePDF(invoice: InvoiceData): Promise<void> {
|
|||||||
throw new Error("Client information is required");
|
throw new Error("Client information is required");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate PDF blob with timeout
|
// Generate PDF blob
|
||||||
const pdfPromise = pdf(<InvoicePDF invoice={invoice} />).toBlob();
|
const blob = await pdf(<InvoicePDF invoice={invoice} />).toBlob();
|
||||||
const timeoutPromise = new Promise<never>((_, reject) =>
|
|
||||||
setTimeout(() => reject(new Error("PDF generation timed out")), 30000),
|
|
||||||
);
|
|
||||||
|
|
||||||
const blob = await Promise.race([pdfPromise, timeoutPromise]);
|
|
||||||
|
|
||||||
// Validate blob
|
// Validate blob
|
||||||
if (!blob || blob.size === 0) {
|
if (!blob || blob.size === 0) {
|
||||||
@@ -1102,22 +1053,8 @@ export async function generateInvoicePDF(invoice: InvoiceData): Promise<void> {
|
|||||||
// Download the PDF
|
// Download the PDF
|
||||||
saveAs(blob, filename);
|
saveAs(blob, filename);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Provide more specific error messages
|
// Log the actual error for debugging
|
||||||
if (error instanceof Error) {
|
console.error("PDF generation error:", error);
|
||||||
if (error.message.includes("timeout")) {
|
|
||||||
throw new Error("PDF generation took too long. Please try again.");
|
|
||||||
} else if (error.message.includes("empty")) {
|
|
||||||
throw new Error("Generated PDF is invalid. Please try again.");
|
|
||||||
} else if (error.message.includes("required")) {
|
|
||||||
throw new Error(error.message);
|
|
||||||
} else if (
|
|
||||||
error.message.includes("font") ||
|
|
||||||
error.message.includes("Font")
|
|
||||||
) {
|
|
||||||
throw new Error("Font loading error. Please try again.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error("Failed to generate PDF. Please try again.");
|
throw new Error("Failed to generate PDF. Please try again.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1127,9 +1064,6 @@ export async function generateInvoicePDFBlob(
|
|||||||
invoice: InvoiceData,
|
invoice: InvoiceData,
|
||||||
): Promise<Blob> {
|
): Promise<Blob> {
|
||||||
try {
|
try {
|
||||||
// Ensure fonts are registered (important for server-side generation)
|
|
||||||
registerFonts();
|
|
||||||
|
|
||||||
// Validate invoice data
|
// Validate invoice data
|
||||||
if (!invoice) {
|
if (!invoice) {
|
||||||
throw new Error("Invoice data is required");
|
throw new Error("Invoice data is required");
|
||||||
@@ -1143,43 +1077,20 @@ export async function generateInvoicePDFBlob(
|
|||||||
throw new Error("Client information is required");
|
throw new Error("Client information is required");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate PDF blob with timeout (same as generateInvoicePDF)
|
// Generate PDF blob
|
||||||
const pdfPromise = pdf(<InvoicePDF invoice={invoice} />).toBlob();
|
const blob = await pdf(<InvoicePDF invoice={invoice} />).toBlob();
|
||||||
const timeoutPromise = new Promise<never>((_, reject) =>
|
|
||||||
setTimeout(() => reject(new Error("PDF generation timed out")), 30000),
|
|
||||||
);
|
|
||||||
|
|
||||||
const blob = await Promise.race([pdfPromise, timeoutPromise]);
|
|
||||||
|
|
||||||
// Validate blob
|
// Validate blob
|
||||||
if (!blob || blob.size === 0) {
|
if (!blob || blob.size === 0) {
|
||||||
throw new Error("Generated PDF is empty");
|
throw new Error("Generated PDF is empty");
|
||||||
}
|
}
|
||||||
|
|
||||||
return blob;
|
return blob;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Provide more specific error messages (same as generateInvoicePDF)
|
// Re-throw with consistent error handling
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
if (error.message.includes("timeout")) {
|
throw error;
|
||||||
throw new Error("PDF generation took too long. Please try again.");
|
|
||||||
} else if (error.message.includes("empty")) {
|
|
||||||
throw new Error("Generated PDF is invalid. Please try again.");
|
|
||||||
} else if (error.message.includes("required")) {
|
|
||||||
throw new Error(error.message);
|
|
||||||
} else if (
|
|
||||||
error.message.includes("font") ||
|
|
||||||
error.message.includes("Font")
|
|
||||||
) {
|
|
||||||
throw new Error("Font loading error. Please try again.");
|
|
||||||
} else if (
|
|
||||||
error.message.includes("Cannot resolve") ||
|
|
||||||
error.message.includes("Failed to load")
|
|
||||||
) {
|
|
||||||
throw new Error("Resource loading error during PDF generation.");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
throw new Error("Failed to generate PDF blob");
|
||||||
throw new Error(
|
|
||||||
`Failed to generate PDF for email attachment: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user