| "use client"; |
|
|
| export interface StatementPdfOptions { |
| filename?: string; |
| quality?: number; |
| scale?: number; |
| padding?: number; |
| theme?: string; |
| } |
|
|
| |
| |
| |
| export async function generateStatementPdf( |
| options: StatementPdfOptions = {}, |
| ): Promise<void> { |
| const { |
| filename = "statement.pdf", |
| quality = 1.0, |
| scale = 4, |
| padding = 10, |
| theme, |
| } = options; |
|
|
| |
| const resolvedTheme = |
| theme === "system" || !theme |
| ? window.matchMedia("(prefers-color-scheme: dark)").matches |
| ? "dark" |
| : "light" |
| : theme; |
|
|
| const backgroundColor = resolvedTheme === "dark" ? "#0c0c0c" : "#ffffff"; |
| const backgroundRgb = |
| resolvedTheme === "dark" |
| ? { r: 12, g: 12, b: 12 } |
| : { r: 255, g: 255, b: 255 }; |
|
|
| try { |
| |
| const html2canvas = (await import("html2canvas")).default; |
|
|
| |
| const statementContent = getStatementContent(); |
| if (!statementContent) { |
| throw new Error("Statement content not found"); |
| } |
|
|
| |
| const elementHeight = |
| statementContent.scrollHeight || statementContent.offsetHeight; |
| const extraHeight = 100; |
|
|
| |
| const canvas = await html2canvas(statementContent, { |
| scale, |
| backgroundColor, |
| useCORS: true, |
| allowTaint: true, |
| logging: false, |
| removeContainer: true, |
| imageTimeout: 0, |
| |
| height: elementHeight + extraHeight, |
| |
| scrollX: 0, |
| scrollY: 0, |
| foreignObjectRendering: false, |
| onclone: (clonedDoc) => { |
| |
| const mainContainer = clonedDoc.querySelector( |
| "[data-statement-content]", |
| ); |
| if (mainContainer) { |
| const containerEl = mainContainer as HTMLElement; |
| containerEl.style.backgroundColor = backgroundColor; |
| |
| containerEl.style.overflow = "visible"; |
| containerEl.style.height = "auto"; |
| containerEl.style.maxHeight = "none"; |
| } |
|
|
| |
| if (clonedDoc.body) { |
| clonedDoc.body.style.backgroundColor = backgroundColor; |
| clonedDoc.body.style.overflow = "visible"; |
| clonedDoc.body.style.height = "auto"; |
| } |
| if (clonedDoc.documentElement) { |
| clonedDoc.documentElement.style.backgroundColor = backgroundColor; |
| clonedDoc.documentElement.style.overflow = "visible"; |
| clonedDoc.documentElement.style.height = "auto"; |
| } |
|
|
| |
| const allTextElements = clonedDoc.querySelectorAll( |
| "svg text, svg tspan", |
| ); |
|
|
| for (const element of allTextElements) { |
| (element as HTMLElement).style.fontFamily = |
| "Hedvig Letters Sans, system-ui, sans-serif"; |
| (element as HTMLElement).style.fontSize = "10px"; |
| } |
|
|
| |
| const hideElements = clonedDoc.querySelectorAll( |
| '[data-hide-in-pdf="true"]', |
| ); |
|
|
| for (const element of hideElements) { |
| const htmlElement = element as HTMLElement; |
| htmlElement.style.display = "none"; |
| |
| if (htmlElement.tagName === "TD" || htmlElement.tagName === "TH") { |
| htmlElement.style.visibility = "hidden"; |
| htmlElement.style.width = "0"; |
| htmlElement.style.padding = "0"; |
| htmlElement.style.border = "none"; |
| } |
| } |
|
|
| |
| const showElements = clonedDoc.querySelectorAll( |
| '[data-show-in-pdf="true"]', |
| ); |
|
|
| for (const element of showElements) { |
| (element as HTMLElement).style.display = "block"; |
| } |
|
|
| |
| const allElements = clonedDoc.querySelectorAll("*"); |
| for (const element of allElements) { |
| const htmlElement = element as HTMLElement; |
| htmlElement.style.animation = "none"; |
| htmlElement.style.transition = "none"; |
| htmlElement.style.opacity = "1"; |
| } |
| }, |
| }); |
|
|
| |
| await createPdfFromCanvas(canvas, { |
| filename, |
| quality, |
| padding, |
| backgroundRgb, |
| }); |
| } catch (error) { |
| console.error("Error generating PDF:", error); |
| throw new Error("Failed to generate PDF from statement"); |
| } |
| } |
|
|
| |
| |
| |
| export async function generateStatementPdfBlob( |
| options: StatementPdfOptions = {}, |
| ): Promise<Blob> { |
| const { |
| filename = "statement.pdf", |
| quality = 1.0, |
| scale = 4, |
| padding = 10, |
| theme, |
| } = options; |
|
|
| |
| const resolvedTheme = |
| theme === "system" || !theme |
| ? window.matchMedia("(prefers-color-scheme: dark)").matches |
| ? "dark" |
| : "light" |
| : theme; |
|
|
| const backgroundColor = resolvedTheme === "dark" ? "#0c0c0c" : "#ffffff"; |
| const backgroundRgb = |
| resolvedTheme === "dark" |
| ? { r: 12, g: 12, b: 12 } |
| : { r: 255, g: 255, b: 255 }; |
|
|
| try { |
| |
| const html2canvas = (await import("html2canvas")).default; |
|
|
| |
| const statementContent = getStatementContent(); |
| if (!statementContent) { |
| throw new Error("Statement content not found"); |
| } |
|
|
| |
| const elementHeight = |
| statementContent.scrollHeight || statementContent.offsetHeight; |
| const extraHeight = 100; |
|
|
| |
| const canvas = await html2canvas(statementContent, { |
| scale, |
| backgroundColor, |
| useCORS: true, |
| allowTaint: true, |
| logging: false, |
| removeContainer: true, |
| imageTimeout: 0, |
| |
| height: elementHeight + extraHeight, |
| |
| scrollX: 0, |
| scrollY: 0, |
| foreignObjectRendering: false, |
| onclone: (clonedDoc) => { |
| |
| const mainContainer = clonedDoc.querySelector( |
| "[data-statement-content]", |
| ); |
| if (mainContainer) { |
| const containerEl = mainContainer as HTMLElement; |
| containerEl.style.backgroundColor = backgroundColor; |
| |
| containerEl.style.overflow = "visible"; |
| containerEl.style.height = "auto"; |
| containerEl.style.maxHeight = "none"; |
| } |
|
|
| |
| if (clonedDoc.body) { |
| clonedDoc.body.style.backgroundColor = backgroundColor; |
| clonedDoc.body.style.overflow = "visible"; |
| clonedDoc.body.style.height = "auto"; |
| } |
| if (clonedDoc.documentElement) { |
| clonedDoc.documentElement.style.backgroundColor = backgroundColor; |
| clonedDoc.documentElement.style.overflow = "visible"; |
| clonedDoc.documentElement.style.height = "auto"; |
| } |
|
|
| |
| const allTextElements = clonedDoc.querySelectorAll( |
| "svg text, svg tspan", |
| ); |
|
|
| for (const element of allTextElements) { |
| (element as HTMLElement).style.fontFamily = |
| "Hedvig Letters Sans, system-ui, sans-serif"; |
| (element as HTMLElement).style.fontSize = "10px"; |
| } |
|
|
| |
| const hideElements = clonedDoc.querySelectorAll( |
| '[data-hide-in-pdf="true"]', |
| ); |
|
|
| for (const element of hideElements) { |
| const htmlElement = element as HTMLElement; |
| htmlElement.style.display = "none"; |
| |
| if (htmlElement.tagName === "TD" || htmlElement.tagName === "TH") { |
| htmlElement.style.visibility = "hidden"; |
| htmlElement.style.width = "0"; |
| htmlElement.style.padding = "0"; |
| htmlElement.style.border = "none"; |
| } |
| } |
|
|
| |
| const showElements = clonedDoc.querySelectorAll( |
| '[data-show-in-pdf="true"]', |
| ); |
|
|
| for (const element of showElements) { |
| (element as HTMLElement).style.display = "block"; |
| } |
|
|
| |
| const allElements = clonedDoc.querySelectorAll("*"); |
| for (const element of allElements) { |
| const htmlElement = element as HTMLElement; |
| htmlElement.style.animation = "none"; |
| htmlElement.style.transition = "none"; |
| htmlElement.style.opacity = "1"; |
| } |
| }, |
| }); |
|
|
| |
| const blob = await createPdfBlobFromCanvas(canvas, { |
| quality, |
| padding, |
| backgroundRgb, |
| }); |
|
|
| return blob; |
| } catch (error) { |
| console.error("Error generating PDF blob:", error); |
| throw new Error("Failed to generate PDF from statement"); |
| } |
| } |
|
|
| |
| |
| |
| async function createPdfFromCanvas( |
| canvas: HTMLCanvasElement, |
| options: { |
| filename: string; |
| quality: number; |
| padding: number; |
| backgroundRgb: { r: number; g: number; b: number }; |
| }, |
| ): Promise<void> { |
| |
| const { jsPDF } = await import("jspdf"); |
|
|
| |
| const imgData = canvas.toDataURL("image/jpeg", options.quality); |
|
|
| |
| const imgWidth = canvas.width; |
| const imgHeight = canvas.height; |
|
|
| |
| const a4Width = 210; |
| const a4Height = 297; |
|
|
| |
| const scale = (a4Width - options.padding * 2) / imgWidth; |
| const scaledWidth = imgWidth * scale; |
| const scaledHeight = imgHeight * scale; |
|
|
| |
| const pdfHeight = Math.max(scaledHeight + options.padding * 2, a4Height); |
|
|
| const pdf = new jsPDF({ |
| orientation: "portrait", |
| unit: "mm", |
| format: [a4Width, pdfHeight], |
| }); |
|
|
| |
| pdf.setFillColor( |
| options.backgroundRgb.r, |
| options.backgroundRgb.g, |
| options.backgroundRgb.b, |
| ); |
| pdf.rect(0, 0, a4Width, pdfHeight, "F"); |
|
|
| |
| pdf.addImage( |
| imgData, |
| "JPEG", |
| options.padding, |
| options.padding, |
| scaledWidth, |
| scaledHeight, |
| ); |
|
|
| |
| pdf.setProperties({ |
| title: "Customer Statement", |
| subject: "Generated from Midday Dashboard", |
| author: "Midday", |
| creator: "Midday Dashboard", |
| }); |
|
|
| pdf.save(options.filename); |
| } |
|
|
| |
| |
| |
| async function createPdfBlobFromCanvas( |
| canvas: HTMLCanvasElement, |
| options: { |
| quality: number; |
| padding: number; |
| backgroundRgb: { r: number; g: number; b: number }; |
| }, |
| ): Promise<Blob> { |
| |
| const { jsPDF } = await import("jspdf"); |
|
|
| |
| const imgData = canvas.toDataURL("image/jpeg", options.quality); |
|
|
| |
| const imgWidth = canvas.width; |
| const imgHeight = canvas.height; |
|
|
| |
| const a4Width = 210; |
| const a4Height = 297; |
|
|
| |
| const scale = (a4Width - options.padding * 2) / imgWidth; |
| const scaledWidth = imgWidth * scale; |
| const scaledHeight = imgHeight * scale; |
|
|
| |
| const pdfHeight = Math.max(scaledHeight + options.padding * 2, a4Height); |
|
|
| const pdf = new jsPDF({ |
| orientation: "portrait", |
| unit: "mm", |
| format: [a4Width, pdfHeight], |
| }); |
|
|
| |
| pdf.setFillColor( |
| options.backgroundRgb.r, |
| options.backgroundRgb.g, |
| options.backgroundRgb.b, |
| ); |
| pdf.rect(0, 0, a4Width, pdfHeight, "F"); |
|
|
| |
| pdf.addImage( |
| imgData, |
| "JPEG", |
| options.padding, |
| options.padding, |
| scaledWidth, |
| scaledHeight, |
| ); |
|
|
| |
| pdf.setProperties({ |
| title: "Customer Statement", |
| subject: "Generated from Midday Dashboard", |
| author: "Midday", |
| creator: "Midday Dashboard", |
| }); |
|
|
| return pdf.output("blob"); |
| } |
|
|
| |
| |
| |
| function getStatementContent(): HTMLElement | null { |
| const selectors = ["[data-statement-content]"]; |
|
|
| for (const selector of selectors) { |
| const element = document.querySelector(selector) as HTMLElement; |
| if (element && element.offsetHeight > 0 && element.offsetWidth > 0) { |
| return element; |
| } |
| } |
|
|
| console.warn("Statement content not found"); |
| return null; |
| } |
|
|