@maistik/nuxt-pdf
Version:
A Nuxt 3 module for server-side PDF generation using Handlebars templates
186 lines (185 loc) • 6.57 kB
JavaScript
import { useRuntimeConfig } from "#imports";
import Handlebars from "handlebars";
export function compilePdfComponent(templateName, ctx, messages, templateSources, partialSources) {
const handlebars = Handlebars.create();
handlebars.registerHelper("t", function(key) {
const keys = key.split(".");
let value = messages;
for (const k of keys) {
if (value && typeof value === "object" && k in value) {
value = value[k];
} else {
return key;
}
}
return typeof value === "string" ? value : key;
});
handlebars.registerHelper("formatCurrency", function(value, currencyOrOptions) {
if (typeof value !== "number") {
return value;
}
let currency = "USD";
if (typeof currencyOrOptions === "string") {
currency = currencyOrOptions;
} else if (currencyOrOptions && typeof currencyOrOptions.hash?.currency === "string") {
currency = currencyOrOptions.hash.currency;
}
if (!/^[A-Z]{3}$/.test(currency)) {
currency = "USD";
}
return new Intl.NumberFormat(ctx.locale || "en-US", {
style: "currency",
currency
}).format(value);
});
handlebars.registerHelper("lineTotal", function(qty, price) {
return (qty * price).toFixed(2);
});
handlebars.registerHelper("eq", function(a, b, options) {
const isEqual = a === b;
if (options && typeof options.fn === "function") {
return isEqual ? options.fn(this) : options.inverse(this);
}
return isEqual;
});
handlebars.registerHelper(
"ne",
(a, b, opts) => a !== b ? opts.fn(this) : opts.inverse(this)
);
handlebars.registerHelper(
"gt",
(a, b, opts) => a > b ? opts.fn(this) : opts.inverse(this)
);
handlebars.registerHelper("formatDate", function(date, formatOrOptions) {
let style = "short";
if (typeof formatOrOptions === "string") {
style = formatOrOptions;
} else if (formatOrOptions && typeof formatOrOptions.hash?.format === "string") {
style = formatOrOptions.hash.format;
}
if (!["full", "long", "medium", "short"].includes(style)) {
style = "short";
}
let dateObj;
if (typeof date === "string") {
dateObj = new Date(date);
} else if (date instanceof Date) {
dateObj = date;
} else {
dateObj = /* @__PURE__ */ new Date();
}
if (isNaN(dateObj.getTime())) {
dateObj = /* @__PURE__ */ new Date();
}
return new Intl.DateTimeFormat(ctx.locale || "en-US", {
dateStyle: style
}).format(dateObj);
});
handlebars.registerHelper("formatNumber", function(value, options = {}) {
if (typeof value !== "number") return value;
return new Intl.NumberFormat(ctx.locale || "en-US", options).format(value);
});
try {
const config = useRuntimeConfig();
const customHelpers = config.pdf.customHelpers;
if (customHelpers && typeof customHelpers === "object") {
Object.entries(customHelpers).forEach(([name, helper]) => {
if (typeof helper === "function") {
handlebars.registerHelper(name, helper);
}
});
}
} catch (error) {
console.warn("Failed to load custom helpers:", error);
}
handlebars.registerHelper("upper", function(str) {
return String(str || "").toUpperCase();
});
handlebars.registerHelper("lower", function(str) {
return String(str || "").toLowerCase();
});
handlebars.registerHelper("capitalize", function(str) {
return String(str || "").charAt(0).toUpperCase() + String(str || "").slice(1).toLowerCase();
});
handlebars.registerHelper("truncate", function(str, length) {
const text = String(str || "");
return text.length > length ? `${text.substring(0, length)}...` : text;
});
handlebars.registerHelper("multiply", function(a, b) {
return (a || 0) * (b || 0);
});
handlebars.registerHelper("add", function(a, b) {
return (a || 0) + (b || 0);
});
handlebars.registerHelper("subtract", function(a, b) {
return (a || 0) - (b || 0);
});
handlebars.registerHelper("divide", function(a, b) {
return b !== 0 ? (a || 0) / b : 0;
});
handlebars.registerHelper("percentage", function(value, total) {
return total !== 0 ? `${((value || 0) / total * 100).toFixed(2)}%` : "0%";
});
Object.entries(partialSources).forEach(([name, source]) => {
handlebars.registerPartial(name, source);
});
const templateSource = templateSources[templateName];
if (!templateSource) {
throw new Error(`Template "${templateName}" not found`);
}
const template = handlebars.compile(templateSource);
const enrichedData = enrichContextForTemplate(templateName, ctx.data);
return template({
...enrichedData,
options: ctx.options,
locale: ctx.locale
});
}
function enrichContextForTemplate(templateName, data) {
const enriched = { ...data };
if (templateName.toLowerCase().includes("invoice")) {
if (enriched.items && Array.isArray(enriched.items)) {
enriched.subtotal = enriched.items.reduce((sum, item) => {
return sum + item.quantity * item.price;
}, 0);
enriched.tax = enriched.subtotal * (enriched.taxRate || 0.1);
enriched.total = enriched.subtotal + enriched.tax;
if (enriched.issueDate && enriched.paymentTerms) {
const issueDate = new Date(enriched.issueDate);
const dueDate = new Date(issueDate);
dueDate.setDate(dueDate.getDate() + enriched.paymentTerms);
enriched.dueDate = dueDate;
}
}
}
if (templateName.toLowerCase().includes("salesreport")) {
if (enriched.salesData && Array.isArray(enriched.salesData)) {
enriched.quarters = calculateQuarters(enriched.salesData);
const totalSales = enriched.salesData.reduce((sum, item) => sum + item.amount, 0);
enriched.rating = calculatePerformanceRating(totalSales);
}
}
return enriched;
}
function calculateQuarters(salesData) {
const quarters = [
{ name: "Q1", months: [0, 1, 2], total: 0 },
{ name: "Q2", months: [3, 4, 5], total: 0 },
{ name: "Q3", months: [6, 7, 8], total: 0 },
{ name: "Q4", months: [9, 10, 11], total: 0 }
];
salesData.forEach((item) => {
const month = new Date(item.date).getMonth();
const quarter = quarters.find((q) => q.months.includes(month));
if (quarter) {
quarter.total += item.amount;
}
});
return quarters;
}
function calculatePerformanceRating(totalSales) {
if (totalSales >= 1e5) return "Excellent";
if (totalSales >= 75e3) return "Good";
if (totalSales >= 5e4) return "Average";
return "Needs Improvement";
}