Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com>
This commit is contained in:
gpt-engineer-app[bot]
2026-04-18 03:16:40 +00:00
co-authored by renee-png
parent e298bd85b3
commit 06843111f7
+108 -27
View File
@@ -16,6 +16,7 @@ import {
type HomeownerLite,
type FirmInfo,
} from "@/lib/forms-shared";
import { supabase } from "@/integrations/supabase/client";
import { jsPDF } from "jspdf";
import { toast } from "sonner";
@@ -28,18 +29,54 @@ If you have any questions, please contact our office.
Sincerely,
{{firmName}}`;
async function loadLogoDataUrl(path: string | null | undefined): Promise<string | null> {
if (!path) return null;
const { data: signed } = await supabase.storage.from("firm-logos").createSignedUrl(path, 60);
if (!signed?.signedUrl) return null;
try {
const res = await fetch(signed.signedUrl);
const blob = await res.blob();
return await new Promise<string>((resolve) => {
const r = new FileReader();
r.onload = () => resolve(r.result as string);
r.readAsDataURL(blob);
});
} catch {
return null;
}
}
export function LetterGenerator() {
const [client, setClient] = useState<ClientLite | null>(null);
const [homeowner, setHomeowner] = useState<HomeownerLite | null>(null);
const [clientId, setClientId] = useState("");
const [homeownerId, setHomeownerId] = useState("");
const [firm, setFirm] = useState<FirmInfo | null>(null);
const [logoDataUrl, setLogoDataUrl] = useState<string | null>(null);
const [attorneyName, setAttorneyName] = useState("");
const [subject, setSubject] = useState("");
const [body, setBody] = useState(DEFAULT_BODY);
const [date, setDate] = useState(new Date().toISOString().slice(0, 10));
useEffect(() => {
fetchFirm().then(setFirm);
fetchFirm().then(async (f) => {
setFirm(f);
const url = await loadLogoDataUrl((f as any)?.logo_storage_path);
setLogoDataUrl(url);
});
supabase.auth.getUser().then(async ({ data }) => {
const uid = data.user?.id;
if (!uid) return;
const { data: prof } = await supabase
.from("profiles")
.select("full_name,title")
.eq("id", uid)
.maybeSingle();
if (prof?.full_name) {
const t = prof.title ? `, ${prof.title}` : "";
setAttorneyName(`${prof.full_name}${t}`);
}
});
}, []);
const handleExport = () => {
@@ -50,52 +87,88 @@ export function LetterGenerator() {
const pageW = doc.internal.pageSize.getWidth();
const margin = 60;
const maxW = pageW - margin * 2;
const centerX = pageW / 2;
doc.setFont("helvetica", "bold");
doc.setFontSize(13);
if (firm?.company_name) doc.text(firm.company_name, margin, 60);
doc.setFont("helvetica", "normal");
doc.setFontSize(9);
const firmLines = [
let y = 50;
// Centered logo
if (logoDataUrl) {
try {
const imgW = 50;
const imgH = 50;
doc.addImage(logoDataUrl, "PNG", centerX - imgW / 2, y, imgW, imgH);
y += imgH + 8;
} catch {
/* ignore */
}
}
// Firm name (left) + Attorney name italic (right) on same line
const firmName = (firm?.company_name ?? "").toUpperCase();
doc.setFont("times", "bold");
doc.setFontSize(12);
if (firmName) doc.text(firmName, margin, y);
if (attorneyName) {
doc.setFont("times", "italic");
doc.setFontSize(11);
doc.text(attorneyName, pageW - margin, y, { align: "right" });
}
y += 14;
// Contact row centered, dot-separated
const contactParts: string[] = [];
const addr = [
firm?.address_line1,
[firm?.city, firm?.state, firm?.postal_code].filter(Boolean).join(", "),
firm?.contact_phone,
firm?.contact_email,
].filter(Boolean) as string[];
let y = 75;
firmLines.forEach((l) => {
doc.text(l, margin, y);
y += 11;
});
y += 20;
]
.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("times", "normal");
doc.setFontSize(10);
doc.text(fmtDateLong(date), margin, y);
if (contactParts.length) {
doc.text(contactParts.join(" \u00B7 "), centerX, y, { align: "center" });
y += 8;
}
// Horizontal rule
doc.setLineWidth(0.75);
doc.line(margin, y, pageW - margin, y);
y += 28;
// Centered bold date
doc.setFont("times", "bold");
doc.setFontSize(11);
doc.text(fmtDateLong(date), centerX, y, { align: "center" });
y += 28;
// Recipient block
doc.setFont("times", "normal");
doc.setFontSize(11);
ownerMailingLines(homeowner).forEach((l) => {
doc.text(l, margin, y);
y += 12;
y += 13;
});
if (!homeowner && client) {
doc.text(client.name, margin, y);
y += 12;
y += 13;
clientAddressLines(client).forEach((l) => {
doc.text(l, margin, y);
y += 12;
y += 13;
});
}
y += 20;
y += 18;
if (renderedSubject) {
doc.setFont("helvetica", "bold");
doc.setFont("times", "bold");
const subjLines = doc.splitTextToSize(`Re: ${renderedSubject}`, maxW);
doc.text(subjLines, margin, y);
y += subjLines.length * 12 + 14;
doc.setFont("helvetica", "normal");
y += subjLines.length * 13 + 14;
doc.setFont("times", "normal");
}
doc.setFontSize(11);
const bodyLines = doc.splitTextToSize(renderedBody, maxW);
for (const line of bodyLines) {
if (y > 740) {
@@ -126,11 +199,19 @@ export function LetterGenerator() {
setHomeowner(h);
}}
/>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<Label>Date</Label>
<Input type="date" value={date} onChange={(e) => setDate(e.target.value)} />
</div>
<div>
<Label>Attorney name (top right)</Label>
<Input
value={attorneyName}
onChange={(e) => setAttorneyName(e.target.value)}
placeholder="Jane Doe, Esq."
/>
</div>
<div>
<Label>Subject (Re:)</Label>
<Input value={subject} onChange={(e) => setSubject(e.target.value)} />