diff --git a/src/components/forms/letter-generator.tsx b/src/components/forms/letter-generator.tsx index 3a9e027..8342947 100644 --- a/src/components/forms/letter-generator.tsx +++ b/src/components/forms/letter-generator.tsx @@ -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 = { + "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"); };