Changes
Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com>
This commit is contained in:
co-authored by
renee-png
parent
2f39feea64
commit
e204646b2a
@@ -83,7 +83,31 @@ export function LetterGenerator() {
|
||||
const renderedBody = applyVariables(body, ctx);
|
||||
const renderedSubject = applyVariables(subject, ctx);
|
||||
|
||||
// Use the template's font / size / margin choices for the body.
|
||||
// Build firm token map (used by both letterhead and footer).
|
||||
const firmAddress = [
|
||||
firm?.address_line1,
|
||||
firm?.address_line2,
|
||||
[firm?.city, firm?.state, firm?.postal_code].filter(Boolean).join(", "),
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(", ");
|
||||
const tokenMap: Record<string, string> = {
|
||||
"firm.name": firm?.company_name ?? "",
|
||||
"firm.address": firmAddress,
|
||||
"firm.address1": firm?.address_line1 ?? "",
|
||||
"firm.address2": firm?.address_line2 ?? "",
|
||||
"firm.city": firm?.city ?? "",
|
||||
"firm.state": firm?.state ?? "",
|
||||
"firm.postal": firm?.postal_code ?? "",
|
||||
"firm.phone": firm?.contact_phone ?? "",
|
||||
"firm.email": firm?.contact_email ?? "",
|
||||
"firm.website": (firm as any)?.website ?? "",
|
||||
"owner.name": homeowner ? `${homeowner.first_name} ${homeowner.last_name}`.trim() : "",
|
||||
"client.name": client?.name ?? "",
|
||||
attorney: attorneyName,
|
||||
date: fmtDateLong(date),
|
||||
};
|
||||
|
||||
const bodyFont = (template.font ?? "times") as "helvetica" | "times" | "courier";
|
||||
const bodyFontSize = template.fontSizePt ?? 11;
|
||||
const margin = template.marginPt ?? 60;
|
||||
@@ -91,64 +115,95 @@ export function LetterGenerator() {
|
||||
|
||||
const doc = new jsPDF({ unit: "pt", format: "letter" });
|
||||
const pageW = doc.internal.pageSize.getWidth();
|
||||
const pageH = doc.internal.pageSize.getHeight();
|
||||
const maxW = pageW - margin * 2;
|
||||
const centerX = pageW / 2;
|
||||
|
||||
// ----- Letterhead -----
|
||||
const lhAlign = template.letterheadAlign ?? "center";
|
||||
const lhFontSize = template.letterheadFontSizePt ?? 10;
|
||||
const logoPos = template.logoPosition ?? "above-center";
|
||||
const logoW = template.logoWidthPt ?? 50;
|
||||
const logoH = logoW; // square assumption; jsPDF will scale if not perfectly square
|
||||
const letterheadLines = (template.letterhead ?? "")
|
||||
.split("\n")
|
||||
.map((l) => fillTokens(l, tokenMap));
|
||||
|
||||
let y = 50;
|
||||
|
||||
// Centered logo
|
||||
if (logoDataUrl) {
|
||||
// Logo above
|
||||
const hasLogo =
|
||||
logoDataUrl && logoPos !== "none" && (logoPos === "above-center" || logoPos === "above-left");
|
||||
if (hasLogo) {
|
||||
try {
|
||||
const imgW = 50;
|
||||
const imgH = 50;
|
||||
doc.addImage(logoDataUrl, "PNG", centerX - imgW / 2, y, imgW, imgH);
|
||||
y += imgH + 8;
|
||||
const lx = logoPos === "above-center" ? centerX - logoW / 2 : margin;
|
||||
doc.addImage(logoDataUrl!, "PNG", lx, y, logoW, logoH);
|
||||
y += logoH + 8;
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
|
||||
// Firm name (left) + Attorney name italic (right)
|
||||
const firmName = (firm?.company_name ?? "").toUpperCase();
|
||||
doc.setFont(bodyFont, "bold");
|
||||
doc.setFontSize(12);
|
||||
if (firmName) doc.text(firmName, margin, y);
|
||||
if (attorneyName) {
|
||||
doc.setFont(bodyFont, "italic");
|
||||
doc.setFontSize(11);
|
||||
doc.text(attorneyName, pageW - margin, y, { align: "right" });
|
||||
// Letterhead text (with optional inline left/right logo)
|
||||
const inlineLogoLeft = logoDataUrl && logoPos === "left";
|
||||
const inlineLogoRight = logoDataUrl && logoPos === "right";
|
||||
const textLeft = inlineLogoLeft ? margin + logoW + 12 : margin;
|
||||
const textRight = inlineLogoRight ? pageW - margin - logoW - 12 : pageW - margin;
|
||||
const textCenter = (textLeft + textRight) / 2;
|
||||
const textBlockTop = y;
|
||||
|
||||
if (inlineLogoLeft) {
|
||||
try {
|
||||
doc.addImage(logoDataUrl!, "PNG", margin, y, logoW, logoH);
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
y += 14;
|
||||
if (inlineLogoRight) {
|
||||
try {
|
||||
doc.addImage(logoDataUrl!, "PNG", pageW - margin - logoW, y, logoW, logoH);
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
// Contact row centered, dot-separated
|
||||
const contactParts: string[] = [];
|
||||
const addr = [
|
||||
firm?.address_line1,
|
||||
[firm?.city, firm?.state, firm?.postal_code].filter(Boolean).join(", "),
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(", ");
|
||||
if (addr) contactParts.push(addr);
|
||||
if (firm?.contact_phone) contactParts.push(firm.contact_phone);
|
||||
if (firm?.contact_email) contactParts.push(firm.contact_email);
|
||||
doc.setFont(bodyFont, "normal");
|
||||
doc.setFontSize(10);
|
||||
if (contactParts.length) {
|
||||
doc.text(contactParts.join(" \u00B7 "), centerX, y, { align: "center" });
|
||||
y += 8;
|
||||
doc.setFontSize(lhFontSize);
|
||||
letterheadLines.forEach((line, i) => {
|
||||
// First line treated as the firm name → bold + slightly larger.
|
||||
if (i === 0 && line.trim()) {
|
||||
doc.setFont(bodyFont, "bold");
|
||||
doc.setFontSize(lhFontSize + 2);
|
||||
} else {
|
||||
doc.setFont(bodyFont, "normal");
|
||||
doc.setFontSize(lhFontSize);
|
||||
}
|
||||
if (!line.trim()) {
|
||||
y += lhFontSize;
|
||||
return;
|
||||
}
|
||||
const x =
|
||||
lhAlign === "left" ? textLeft : lhAlign === "right" ? textRight : textCenter;
|
||||
const opts = { align: lhAlign } as const;
|
||||
doc.text(line, x, y, opts as any);
|
||||
y += (i === 0 ? lhFontSize + 4 : lhFontSize) + 2;
|
||||
});
|
||||
|
||||
// Make sure y clears any inline logo
|
||||
if (inlineLogoLeft || inlineLogoRight) {
|
||||
y = Math.max(y, textBlockTop + logoH);
|
||||
}
|
||||
y += 6;
|
||||
|
||||
if (template.letterheadRule !== false) {
|
||||
doc.setLineWidth(0.75);
|
||||
doc.line(margin, y, pageW - margin, y);
|
||||
y += 22;
|
||||
}
|
||||
|
||||
doc.setLineWidth(0.75);
|
||||
doc.line(margin, y, pageW - margin, y);
|
||||
y += 28;
|
||||
|
||||
// Centered bold date
|
||||
// ----- Date (centered, bold) -----
|
||||
doc.setFont(bodyFont, "bold");
|
||||
doc.setFontSize(11);
|
||||
doc.text(fmtDateLong(date), centerX, y, { align: "center" });
|
||||
y += 28;
|
||||
|
||||
// Recipient block
|
||||
// ----- Recipient -----
|
||||
doc.setFont(bodyFont, "normal");
|
||||
doc.setFontSize(bodyFontSize);
|
||||
ownerMailingLines(homeowner).forEach((l) => {
|
||||
@@ -173,34 +228,68 @@ export function LetterGenerator() {
|
||||
doc.setFont(bodyFont, "normal");
|
||||
}
|
||||
|
||||
// ----- Body (reserve room for footer) -----
|
||||
const footerLines = (template.footer ?? "")
|
||||
.split("\n")
|
||||
.map((l) => fillTokens(l, tokenMap))
|
||||
.filter((l) => l.trim().length > 0);
|
||||
const footerFontSize = template.footerFontSizePt ?? 9;
|
||||
const footerHeight =
|
||||
footerLines.length > 0
|
||||
? footerLines.length * (footerFontSize + 2) + (template.footerRule !== false ? 8 : 0) + 12
|
||||
: 0;
|
||||
const bodyBottomLimit = pageH - margin - footerHeight - 8;
|
||||
|
||||
const bodyLines = doc.splitTextToSize(renderedBody, maxW);
|
||||
for (const line of bodyLines) {
|
||||
if (y > 740) {
|
||||
if (y > bodyBottomLimit) {
|
||||
doc.addPage();
|
||||
y = 60;
|
||||
y = margin;
|
||||
}
|
||||
doc.text(line, margin, y);
|
||||
y += lh;
|
||||
}
|
||||
|
||||
// Optional signature block from the template (admins can edit it).
|
||||
if (template.signature) {
|
||||
y += 18;
|
||||
const sig = fillTokens(template.signature, {
|
||||
"firm.name": firm?.company_name ?? "",
|
||||
"owner.name": homeowner ? `${homeowner.first_name} ${homeowner.last_name}`.trim() : "",
|
||||
"client.name": client?.name ?? "",
|
||||
});
|
||||
const sig = fillTokens(template.signature, tokenMap);
|
||||
sig.split("\n").forEach((line) => {
|
||||
if (y > 740) {
|
||||
if (y > bodyBottomLimit) {
|
||||
doc.addPage();
|
||||
y = 60;
|
||||
y = margin;
|
||||
}
|
||||
doc.text(line, margin, y);
|
||||
y += lh;
|
||||
});
|
||||
}
|
||||
|
||||
// ----- Footer on every page -----
|
||||
if (footerLines.length > 0) {
|
||||
const total = doc.getNumberOfPages();
|
||||
const footerAlign = template.footerAlign ?? "center";
|
||||
for (let p = 1; p <= total; p++) {
|
||||
doc.setPage(p);
|
||||
let fy = pageH - margin - footerHeight + 8;
|
||||
if (template.footerRule !== false) {
|
||||
doc.setLineWidth(0.5);
|
||||
doc.line(margin, fy, pageW - margin, fy);
|
||||
fy += 10;
|
||||
}
|
||||
doc.setFont(bodyFont, "normal");
|
||||
doc.setFontSize(footerFontSize);
|
||||
footerLines.forEach((line) => {
|
||||
const x =
|
||||
footerAlign === "left"
|
||||
? margin
|
||||
: footerAlign === "right"
|
||||
? pageW - margin
|
||||
: centerX;
|
||||
doc.text(line, x, fy, { align: footerAlign } as any);
|
||||
fy += footerFontSize + 2;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
doc.save(`Letter_${date}.pdf`);
|
||||
toast.success("Letter PDF downloaded");
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user