This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
import { h } from "vue";
|
||||
import { Document, Page, Text, View } from "@ceereals/vue-pdf";
|
||||
import { Document, Page, Text, View, Image } from "@ceereals/vue-pdf";
|
||||
import { readFileSync, existsSync } from "fs";
|
||||
import { join } from "path";
|
||||
import type { Style } from "@react-pdf/types";
|
||||
|
||||
interface InvoiceItem {
|
||||
@@ -22,6 +24,7 @@ interface Organization {
|
||||
state: string | null;
|
||||
zip: string | null;
|
||||
country: string | null;
|
||||
logoUrl?: string | null;
|
||||
}
|
||||
|
||||
interface Invoice {
|
||||
@@ -67,6 +70,12 @@ const styles = {
|
||||
flex: 1,
|
||||
maxWidth: 280,
|
||||
} as Style,
|
||||
logo: {
|
||||
height: 40,
|
||||
marginBottom: 8,
|
||||
objectFit: "contain",
|
||||
objectPosition: "left",
|
||||
} as Style,
|
||||
headerRight: {
|
||||
flex: 1,
|
||||
alignItems: "flex-end",
|
||||
@@ -84,40 +93,7 @@ const styles = {
|
||||
lineHeight: 1.5,
|
||||
marginBottom: 12,
|
||||
} as Style,
|
||||
statusBadge: {
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 4,
|
||||
borderRadius: 6,
|
||||
fontSize: 9,
|
||||
fontWeight: "bold",
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: 1,
|
||||
alignSelf: "flex-start",
|
||||
} as Style,
|
||||
statusDraft: {
|
||||
backgroundColor: "#F3F4F6",
|
||||
color: "#6B7280",
|
||||
} as Style,
|
||||
statusSent: {
|
||||
backgroundColor: "#DBEAFE",
|
||||
color: "#1E40AF",
|
||||
} as Style,
|
||||
statusPaid: {
|
||||
backgroundColor: "#D1FAE5",
|
||||
color: "#065F46",
|
||||
} as Style,
|
||||
statusAccepted: {
|
||||
backgroundColor: "#D1FAE5",
|
||||
color: "#065F46",
|
||||
} as Style,
|
||||
statusVoid: {
|
||||
backgroundColor: "#FEE2E2",
|
||||
color: "#991B1B",
|
||||
} as Style,
|
||||
statusDeclined: {
|
||||
backgroundColor: "#FEE2E2",
|
||||
color: "#991B1B",
|
||||
} as Style,
|
||||
|
||||
invoiceTypeContainer: {
|
||||
alignItems: "flex-end",
|
||||
marginBottom: 16,
|
||||
@@ -304,24 +280,6 @@ export function createInvoiceDocument(props: InvoiceDocumentProps) {
|
||||
});
|
||||
};
|
||||
|
||||
const getStatusStyle = (status: string): Style => {
|
||||
const baseStyle = styles.statusBadge;
|
||||
switch (status) {
|
||||
case "draft":
|
||||
return { ...baseStyle, ...styles.statusDraft };
|
||||
case "sent":
|
||||
return { ...baseStyle, ...styles.statusSent };
|
||||
case "paid":
|
||||
case "accepted":
|
||||
return { ...baseStyle, ...styles.statusPaid };
|
||||
case "void":
|
||||
case "declined":
|
||||
return { ...baseStyle, ...styles.statusVoid };
|
||||
default:
|
||||
return { ...baseStyle, ...styles.statusDraft };
|
||||
}
|
||||
};
|
||||
|
||||
return h(Document, [
|
||||
h(
|
||||
Page,
|
||||
@@ -330,6 +288,55 @@ export function createInvoiceDocument(props: InvoiceDocumentProps) {
|
||||
// Header
|
||||
h(View, { style: styles.header }, [
|
||||
h(View, { style: styles.headerLeft }, [
|
||||
(() => {
|
||||
if (organization.logoUrl) {
|
||||
try {
|
||||
let logoPath;
|
||||
// Handle uploads directory which might be external to public/
|
||||
if (organization.logoUrl.startsWith("/uploads/")) {
|
||||
let uploadDir;
|
||||
const envRootDir = process.env.ROOT_DIR
|
||||
? process.env.ROOT_DIR
|
||||
: import.meta.env.ROOT_DIR;
|
||||
|
||||
if (envRootDir) {
|
||||
uploadDir = join(envRootDir, "uploads");
|
||||
} else {
|
||||
uploadDir =
|
||||
process.env.UPLOAD_DIR ||
|
||||
join(process.cwd(), "public", "uploads");
|
||||
}
|
||||
const filename = organization.logoUrl.replace(
|
||||
"/uploads/",
|
||||
"",
|
||||
);
|
||||
logoPath = join(uploadDir, filename);
|
||||
} else {
|
||||
logoPath = join(
|
||||
process.cwd(),
|
||||
"public",
|
||||
organization.logoUrl,
|
||||
);
|
||||
}
|
||||
|
||||
if (existsSync(logoPath)) {
|
||||
const ext = logoPath.split(".").pop()?.toLowerCase();
|
||||
if (ext === "png" || ext === "jpg" || ext === "jpeg") {
|
||||
return h(Image, {
|
||||
src: {
|
||||
data: readFileSync(logoPath),
|
||||
format: ext === "png" ? "png" : "jpg",
|
||||
},
|
||||
style: styles.logo,
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore errors
|
||||
}
|
||||
}
|
||||
return null;
|
||||
})(),
|
||||
h(Text, { style: styles.organizationName }, organization.name),
|
||||
organization.street || organization.city
|
||||
? h(
|
||||
@@ -353,9 +360,6 @@ export function createInvoiceDocument(props: InvoiceDocumentProps) {
|
||||
].filter(Boolean),
|
||||
)
|
||||
: null,
|
||||
h(View, { style: getStatusStyle(invoice.status) }, [
|
||||
h(Text, invoice.status),
|
||||
]),
|
||||
]),
|
||||
h(View, { style: styles.headerRight }, [
|
||||
h(View, { style: styles.invoiceTypeContainer }, [
|
||||
@@ -374,14 +378,16 @@ export function createInvoiceDocument(props: InvoiceDocumentProps) {
|
||||
formatDate(invoice.issueDate),
|
||||
),
|
||||
]),
|
||||
h(View, { style: styles.metaRow }, [
|
||||
h(Text, { style: styles.metaLabel }, "Due Date"),
|
||||
h(
|
||||
Text,
|
||||
{ style: styles.metaValue },
|
||||
formatDate(invoice.dueDate),
|
||||
),
|
||||
]),
|
||||
invoice.type !== "quote"
|
||||
? h(View, { style: styles.metaRow }, [
|
||||
h(Text, { style: styles.metaLabel }, "Due Date"),
|
||||
h(
|
||||
Text,
|
||||
{ style: styles.metaValue },
|
||||
formatDate(invoice.dueDate),
|
||||
),
|
||||
])
|
||||
: null,
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
|
||||
Reference in New Issue
Block a user