Updated letterhead design
X-Lovable-Edit-ID: edt-3bbfd37c-00e8-4dd7-8f7e-5aa84dbb7c42 Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com>
This commit is contained in:
@@ -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)} />
|
||||
|
||||
Reference in New Issue
Block a user