Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com>
This commit is contained in:
gpt-engineer-app[bot]
2026-04-18 18:22:40 +00:00
co-authored by renee-png
parent 2f39feea64
commit e204646b2a
+137 -48
View File
@@ -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");
};