From 0881a0efe4da5d1477f2631790860ad1d081d66f Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Fri, 17 Apr 2026 03:21:27 +0000 Subject: [PATCH 1/5] Changes Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com> --- src/components/forms/form-pickers.tsx | 96 ++++++++++++++++ src/lib/forms-shared.ts | 157 ++++++++++++++++++++++++++ 2 files changed, 253 insertions(+) create mode 100644 src/components/forms/form-pickers.tsx create mode 100644 src/lib/forms-shared.ts diff --git a/src/components/forms/form-pickers.tsx b/src/components/forms/form-pickers.tsx new file mode 100644 index 0000000..d0048d5 --- /dev/null +++ b/src/components/forms/form-pickers.tsx @@ -0,0 +1,96 @@ +import { useEffect, useState } from "react"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Label } from "@/components/ui/label"; +import { + fetchClients, + fetchHomeowners, + type ClientLite, + type HomeownerLite, +} from "@/lib/forms-shared"; + +export function ClientHomeownerPicker({ + clientId, + homeownerId, + onClientChange, + onHomeownerChange, + showHomeowner = true, +}: { + clientId: string; + homeownerId: string; + onClientChange: (id: string, client: ClientLite | null) => void; + onHomeownerChange: (id: string, homeowner: HomeownerLite | null) => void; + showHomeowner?: boolean; +}) { + const [clients, setClients] = useState([]); + const [homeowners, setHomeowners] = useState([]); + + useEffect(() => { + fetchClients().then(setClients); + }, []); + + useEffect(() => { + if (!clientId) { + setHomeowners([]); + return; + } + fetchHomeowners(clientId).then(setHomeowners); + }, [clientId]); + + return ( +
+
+ + +
+ {showHomeowner && ( +
+ + +
+ )} +
+ ); +} diff --git a/src/lib/forms-shared.ts b/src/lib/forms-shared.ts new file mode 100644 index 0000000..b3c2d44 --- /dev/null +++ b/src/lib/forms-shared.ts @@ -0,0 +1,157 @@ +import { supabase } from "@/integrations/supabase/client"; +import { format } from "date-fns"; + +export interface ClientLite { + id: string; + name: string; + address_line1: string | null; + address_line2: string | null; + city: string | null; + state: string | null; + postal_code: string | null; + primary_contact_email: string | null; + primary_contact_phone: string | null; + annual_interest_rate: number | null; +} + +export interface HomeownerLite { + id: string; + client_id: string; + first_name: string; + last_name: string; + unit_number: string | null; + address: string | null; + email: string | null; + phone: string | null; + opening_balance: number; +} + +export async function fetchClients(): Promise { + const { data } = await supabase + .from("clients") + .select( + "id,name,address_line1,address_line2,city,state,postal_code,primary_contact_email,primary_contact_phone,annual_interest_rate", + ) + .order("name"); + return (data ?? []) as ClientLite[]; +} + +export async function fetchHomeowners(clientId: string): Promise { + const { data } = await supabase + .from("homeowners") + .select("id,client_id,first_name,last_name,unit_number,address,email,phone,opening_balance") + .eq("client_id", clientId) + .order("last_name"); + return (data ?? []) as HomeownerLite[]; +} + +export function ownerFullName(h: HomeownerLite | null | undefined): string { + if (!h) return ""; + return `${h.first_name} ${h.last_name}`.trim(); +} + +export function clientAddressLines(c: ClientLite | null | undefined): string[] { + if (!c) return []; + const lines: string[] = []; + if (c.address_line1) lines.push(c.address_line1); + if (c.address_line2) lines.push(c.address_line2); + const cityLine = [c.city, c.state, c.postal_code].filter(Boolean).join(", "); + if (cityLine) lines.push(cityLine); + return lines; +} + +export function ownerMailingLines(h: HomeownerLite | null | undefined): string[] { + if (!h) return []; + const lines: string[] = [ownerFullName(h)]; + if (h.address) lines.push(h.address); + return lines; +} + +export const SYSTEM_VARIABLES = [ + { key: "{{clientName}}", description: "Client / Association name" }, + { key: "{{ownerName}}", description: "Homeowner full name" }, + { key: "{{propertyAddress}}", description: "Homeowner property address" }, + { key: "{{unitNumber}}", description: "Homeowner unit number" }, + { key: "{{accountNumber}}", description: "Account / unit identifier" }, + { key: "{{balance}}", description: "Current balance" }, + { key: "{{currentDate}}", description: "Today's date" }, + { key: "{{firmName}}", description: "Your firm name" }, +] as const; + +export function applyVariables( + body: string, + ctx: { client?: ClientLite | null; homeowner?: HomeownerLite | null; firmName?: string }, +): string { + const today = format(new Date(), "MMMM d, yyyy"); + const replacements: Record = { + "{{clientName}}": ctx.client?.name ?? "", + "{{ownerName}}": ownerFullName(ctx.homeowner), + "{{propertyAddress}}": ctx.homeowner?.address ?? "", + "{{unitNumber}}": ctx.homeowner?.unit_number ?? "", + "{{accountNumber}}": ctx.homeowner?.unit_number ?? "", + "{{balance}}": (ctx.homeowner?.opening_balance ?? 0).toFixed(2), + "{{currentDate}}": today, + "{{firmName}}": ctx.firmName ?? "", + }; + let out = body; + for (const [k, v] of Object.entries(replacements)) { + out = out.split(k).join(v); + } + return out; +} + +export function fmtCurrency(n: number | string | null | undefined): string { + const v = typeof n === "string" ? parseFloat(n) : (n ?? 0); + return (Number.isFinite(v) ? v : 0).toLocaleString("en-US", { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }); +} + +export function fmtDateLong(d: Date | string | null | undefined): string { + if (!d) return ""; + const date = typeof d === "string" ? new Date(d.includes("T") ? d : d + "T12:00:00") : d; + return format(date, "MMMM d, yyyy"); +} + +export interface FirmInfo { + company_name: string | null; + address_line1: string | null; + address_line2: string | null; + city: string | null; + state: string | null; + postal_code: string | null; + contact_email: string | null; + contact_phone: string | null; + website: string | null; +} + +export async function fetchFirm(): Promise { + const { data } = await supabase.from("firm_settings").select("*").maybeSingle(); + return (data as FirmInfo) ?? null; +} + +export async function savePdfToDocuments(opts: { + blob: Blob; + caseId: string; + name: string; + folder?: string; +}) { + const { blob, caseId, name, folder = "Forms & Letters" } = opts; + const path = `${caseId}/${Date.now()}-${name}`; + const { error: upErr } = await supabase.storage + .from("case-documents") + .upload(path, blob, { contentType: "application/pdf", upsert: false }); + if (upErr) throw upErr; + const { data: u } = await supabase.auth.getUser(); + const { error: insErr } = await supabase.from("documents").insert({ + case_id: caseId, + folder, + name, + storage_path: path, + mime_type: "application/pdf", + size_bytes: blob.size, + uploaded_by: u.user?.id ?? null, + }); + if (insErr) throw insErr; +} From c4a0abc898fe8d9f0056c116aed9c3dabbf59d9d Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Fri, 17 Apr 2026 03:24:22 +0000 Subject: [PATCH 2/5] Changes Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com> --- src/components/forms/affidavit-form.tsx | 248 ++++++++++ src/components/forms/custom-form-builder.tsx | 166 +++++++ src/components/forms/estoppel-form.tsx | 466 +++++++++++++++++++ src/components/forms/itl-form.tsx | 242 ++++++++++ src/components/forms/letter-generator.tsx | 163 +++++++ src/components/forms/nola-form.tsx | 219 +++++++++ 6 files changed, 1504 insertions(+) create mode 100644 src/components/forms/affidavit-form.tsx create mode 100644 src/components/forms/custom-form-builder.tsx create mode 100644 src/components/forms/estoppel-form.tsx create mode 100644 src/components/forms/itl-form.tsx create mode 100644 src/components/forms/letter-generator.tsx create mode 100644 src/components/forms/nola-form.tsx diff --git a/src/components/forms/affidavit-form.tsx b/src/components/forms/affidavit-form.tsx new file mode 100644 index 0000000..efe7f54 --- /dev/null +++ b/src/components/forms/affidavit-form.tsx @@ -0,0 +1,248 @@ +import { useEffect, useState } from "react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Card, CardContent } from "@/components/ui/card"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { FileDown } from "lucide-react"; +import { ClientHomeownerPicker } from "./form-pickers"; +import { + fetchFirm, + type ClientLite, + type FirmInfo, +} from "@/lib/forms-shared"; +import { jsPDF } from "jspdf"; +import { format } from "date-fns"; +import { toast } from "sonner"; + +const MEETING_TYPES = [ + "Annual Membership Meeting", + "Special Membership Meeting", + "Board of Directors Meeting", + "Special Board Meeting", + "Budget Meeting", + "Turnover Meeting", + "Election Meeting", +]; +const NOTICE_DAYS = [ + { value: "60", label: "Sixty (60) Days", word: "sixty (60)" }, + { value: "30", label: "Thirty (30) Days", word: "thirty (30)" }, + { value: "14", label: "Fourteen (14) Days", word: "fourteen (14)" }, +]; +const STATUTES = [ + { value: "720", label: "§720.306(1)(d)(5) — HOA", text: "§720.306(1)(d)(5)" }, + { value: "718", label: "§718.112(2)(d)3 — Condo", text: "§718.112(2)(d)3" }, +]; + +function ordinal(day: number) { + const s = ["th", "st", "nd", "rd"]; + const v = day % 100; + return `${day}${s[(v - 20) % 10] || s[v] || s[0]}`; +} + +export function AffidavitForm() { + const [client, setClient] = useState(null); + const [clientId, setClientId] = useState(""); + const [, setFirm] = useState(null); + + const [mailingDate, setMailingDate] = useState(new Date().toISOString().slice(0, 10)); + const [meetingType, setMeetingType] = useState(MEETING_TYPES[0]); + const [noticeDays, setNoticeDays] = useState("14"); + const [statute, setStatute] = useState("720"); + const [signerName, setSignerName] = useState(""); + const [signerTitle, setSignerTitle] = useState("Manager"); + const [signerCredentials, setSignerCredentials] = useState(""); + const [county, setCounty] = useState("Brevard"); + + useEffect(() => { + fetchFirm().then(setFirm); + }, []); + + const handleExport = () => { + if (!client) { + toast.error("Select a client first"); + return; + } + const dateObj = new Date(mailingDate + "T12:00:00"); + const dayWord = ordinal(dateObj.getDate()); + const monthName = format(dateObj, "MMMM"); + const year = format(dateObj, "yyyy"); + const dateWordy = format(dateObj, "MMMM d, yyyy"); + const daysWord = NOTICE_DAYS.find((d) => d.value === noticeDays)?.word ?? noticeDays; + const statuteText = STATUTES.find((s) => s.value === statute)?.text ?? ""; + + const doc = new jsPDF({ unit: "pt", format: "letter" }); + const pageW = doc.internal.pageSize.getWidth(); + const margin = 60; + const contentW = pageW - margin * 2; + let y = 70; + + doc.setFont("helvetica", "bold"); + doc.setFontSize(18); + doc.text("AFFIDAVIT OF MAILING", margin, y); + y += 26; + doc.setFont("helvetica", "bolditalic"); + doc.setFontSize(11); + doc.text(`${year} ${meetingType}`, margin, y); + y += 26; + + doc.setFont("helvetica", "bold"); + doc.setFontSize(10.5); + doc.text("STATE OF FLORIDA", margin, y); + y += 15; + doc.text(`COUNTY OF ${county.toUpperCase()}`, margin, y); + y += 24; + + doc.setFont("helvetica", "normal"); + const body = `I, ${signerName.toUpperCase() || "[SIGNER]"}, on behalf of the Secretary of ${client.name}, being first duly sworn, depose and say that the notice of the ${meetingType.toUpperCase()} was mailed, hand delivered, or electronically sent to each unit owner at the address last furnished to the Association in accordance with the requirements of Section ${statuteText} Florida Statutes, at least ${daysWord} days prior to the noticed meeting, on ${dateWordy}.`; + const lines = doc.splitTextToSize(body, contentW); + doc.text(lines, margin, y); + y += lines.length * 15 + 8; + doc.text(`Dated this ${dayWord} day of ${monthName}, ${year}.`, margin, y); + y += 28; + + doc.setFont("helvetica", "bold"); + doc.text(`BY: ${signerName || "[Signer]"}, ${signerTitle}`, margin, y); + y += 44; + doc.line(margin, y, margin + 250, y); + y += 13; + doc.setFont("helvetica", "italic"); + doc.setFontSize(9); + doc.text(signerCredentials || signerName, margin, y); + + y += 36; + doc.setFont("helvetica", "bold"); + doc.setFontSize(10.5); + doc.text("STATE OF FLORIDA", margin, y); + y += 15; + doc.text(`COUNTY OF ${county.toUpperCase()}`, margin, y); + y += 24; + + doc.setFont("helvetica", "normal"); + const notary = `The foregoing Affidavit was acknowledged before me this ${dayWord} day of ${monthName}, ${year} by ${signerName || "[Signer]"}, who is personally known to me or produced a Florida Driver's License as identification.`; + const nLines = doc.splitTextToSize(notary, contentW); + doc.text(nLines, margin, y); + y += nLines.length * 15 + 24; + + doc.line(margin, y, margin + 270, y); + y += 13; + doc.text("NOTARY PUBLIC", margin, y); + y += 32; + doc.setFont("helvetica", "bold"); + doc.text("(SEAL)", margin, y); + + doc.save(`Affidavit_of_Mailing_${mailingDate}.pdf`); + toast.success("Affidavit PDF downloaded"); + }; + + return ( +
+ + + { + setClientId(id); + setClient(c); + }} + onHomeownerChange={() => {}} + showHomeowner={false} + /> +
+
+ + setMailingDate(e.target.value)} + /> +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+ + setCounty(e.target.value)} /> +
+
+
+
+ + setSignerName(e.target.value)} /> +
+
+ + setSignerTitle(e.target.value)} /> +
+
+ + setSignerCredentials(e.target.value)} + placeholder="e.g. LCAM" + /> +
+
+ +
+ +
+
+
+
+ ); +} diff --git a/src/components/forms/custom-form-builder.tsx b/src/components/forms/custom-form-builder.tsx new file mode 100644 index 0000000..206b478 --- /dev/null +++ b/src/components/forms/custom-form-builder.tsx @@ -0,0 +1,166 @@ +import { useEffect, useRef, useState } from "react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Card, CardContent } from "@/components/ui/card"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { Separator } from "@/components/ui/separator"; +import { Bold, Italic, Underline, FileDown, Search } from "lucide-react"; +import { ClientHomeownerPicker } from "./form-pickers"; +import { + applyVariables, + fetchFirm, + SYSTEM_VARIABLES, + type ClientLite, + type HomeownerLite, + type FirmInfo, +} from "@/lib/forms-shared"; +import { jsPDF } from "jspdf"; +import { toast } from "sonner"; + +export function CustomFormBuilder() { + const editorRef = useRef(null); + const [title, setTitle] = useState("Official Notice"); + const [client, setClient] = useState(null); + const [homeowner, setHomeowner] = useState(null); + const [clientId, setClientId] = useState(""); + const [homeownerId, setHomeownerId] = useState(""); + const [firm, setFirm] = useState(null); + const [search, setSearch] = useState(""); + + useEffect(() => { + fetchFirm().then(setFirm); + }, []); + + const insertVar = (key: string) => { + if (!editorRef.current) return; + editorRef.current.focus(); + document.execCommand("insertText", false, key); + }; + + const exec = (cmd: string) => { + editorRef.current?.focus(); + document.execCommand(cmd, false); + }; + + const handleExport = () => { + const raw = editorRef.current?.innerText ?? ""; + const body = applyVariables(raw, { + client, + homeowner, + firmName: firm?.company_name ?? "", + }); + const doc = new jsPDF({ unit: "pt", format: "letter" }); + const margin = 54; + const maxW = doc.internal.pageSize.getWidth() - margin * 2; + doc.setFont("helvetica", "bold"); + doc.setFontSize(16); + doc.text(applyVariables(title, { client, homeowner, firmName: firm?.company_name ?? "" }), margin, 80); + doc.setFont("helvetica", "normal"); + doc.setFontSize(11); + const lines = doc.splitTextToSize(body || " ", maxW); + doc.text(lines, margin, 110); + doc.save(`${title.replace(/\s+/g, "_")}.pdf`); + toast.success("PDF downloaded"); + }; + + const filtered = SYSTEM_VARIABLES.filter( + (v) => + v.key.toLowerCase().includes(search.toLowerCase()) || + v.description.toLowerCase().includes(search.toLowerCase()), + ); + + return ( +
+ + + { + setClientId(id); + setClient(c); + }} + onHomeownerChange={(id, h) => { + setHomeownerId(id); + setHomeowner(h); + }} + /> +
+ + setTitle(e.target.value)} /> +
+
+
+ +
+ + +
+ +

+ Click to insert at cursor position. +

+
+
+ + setSearch(e.target.value)} + className="pl-8 h-8 text-xs" + /> +
+ +
+ {filtered.map((v) => ( + + ))} +
+
+
+
+ + + +
+ + + + + +
+
+

Start typing here…

+
+
+
+
+
+ ); +} diff --git a/src/components/forms/estoppel-form.tsx b/src/components/forms/estoppel-form.tsx new file mode 100644 index 0000000..4376e6c --- /dev/null +++ b/src/components/forms/estoppel-form.tsx @@ -0,0 +1,466 @@ +import { useEffect, useMemo, useState } from "react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Textarea } from "@/components/ui/textarea"; +import { Card, CardContent } from "@/components/ui/card"; +import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; +import { FileDown } from "lucide-react"; +import { ClientHomeownerPicker } from "./form-pickers"; +import { + fetchFirm, + fmtCurrency, + fmtDateLong, + type ClientLite, + type FirmInfo, +} from "@/lib/forms-shared"; +import { jsPDF } from "jspdf"; +import { toast } from "sonner"; + +function YesNoField({ + label, + value, + onChange, +}: { + label: string; + value: string; + onChange: (v: string) => void; +}) { + return ( +
+ {label} + +
+ + +
+
+ + +
+
+
+ ); +} + +export function EstoppelForm() { + const [client, setClient] = useState(null); + const [clientId, setClientId] = useState(""); + const [firm, setFirm] = useState(null); + + const [date, setDate] = useState(new Date().toISOString().slice(0, 10)); + const [parcelId, setParcelId] = useState(""); + const [recipientName, setRecipientName] = useState(""); + const [recipientAddress, setRecipientAddress] = useState(""); + const [seller, setSeller] = useState(""); + const [buyerBank, setBuyerBank] = useState(""); + const [propertyAddress, setPropertyAddress] = useState(""); + const [legalDescription, setLegalDescription] = useState(""); + + // financials + const [balanceAtClose, setBalanceAtClose] = useState("0.00"); + const [advAssessments, setAdvAssessments] = useState("0.00"); + const [accountStartupFee, setAccountStartupFee] = useState("0.00"); + const [delinquencyFee, setDelinquencyFee] = useState("0.00"); + const [estoppelFee, setEstoppelFee] = useState("250.00"); + + // q10 payoff + const [q10Unpaid, setQ10Unpaid] = useState("0.00"); + const [q10Interest, setQ10Interest] = useState("0.00"); + const [q10Late, setQ10Late] = useState("0.00"); + const [q10Admin, setQ10Admin] = useState("0.00"); + const [q10Legal, setQ10Legal] = useState("0.00"); + + // questionnaire + const [q1, setQ1] = useState("no"); + const [q4, setQ4] = useState("no"); + const [q6, setQ6] = useState("no"); + const [q8, setQ8] = useState("no"); + const [q17, setQ17] = useState("no"); + const [q24, setQ24] = useState("no"); + const [q26, setQ26] = useState("no"); + + const [additionalInfo, setAdditionalInfo] = useState(""); + + useEffect(() => { + fetchFirm().then(setFirm); + }, []); + + const total1 = + (parseFloat(balanceAtClose) || 0) + (parseFloat(advAssessments) || 0); + const total2 = + (parseFloat(accountStartupFee) || 0) + (parseFloat(delinquencyFee) || 0); + const q10Sub = + (parseFloat(q10Unpaid) || 0) + + (parseFloat(q10Interest) || 0) + + (parseFloat(q10Late) || 0) + + (parseFloat(q10Admin) || 0) + + (parseFloat(q10Legal) || 0); + const grandTotal = useMemo( + () => total1 + total2 + (parseFloat(estoppelFee) || 0), + [total1, total2, estoppelFee], + ); + + const handleExport = () => { + if (!client) { + toast.error("Select a client first"); + return; + } + const doc = new jsPDF({ unit: "pt", format: "letter" }); + const pageW = doc.internal.pageSize.getWidth(); + const margin = 54; + let y = 60; + + // Header + doc.setFont("helvetica", "bold"); + doc.setFontSize(16); + doc.text("ESTOPPEL CERTIFICATE", margin, y); + y += 22; + doc.setFont("helvetica", "normal"); + doc.setFontSize(10); + doc.text(client.name, margin, y); + y += 12; + if (firm?.company_name) { + doc.text(`c/o ${firm.company_name}`, margin, y); + y += 12; + } + doc.setFont("helvetica", "bold"); + doc.text(fmtDateLong(date), pageW - margin - doc.getTextWidth(fmtDateLong(date)), 60); + + y += 16; + doc.setFont("helvetica", "normal"); + const recipientLines = [ + `TO: ${recipientName}`, + ...recipientAddress.split("\n").filter(Boolean), + ]; + recipientLines.forEach((l) => { + doc.text(l, margin, y); + y += 13; + }); + + y += 10; + const meta: [string, string][] = [ + ["Parcel ID:", parcelId], + ["Seller:", seller], + ["Buyer / Bank:", buyerBank], + ["Property Address:", propertyAddress], + ]; + meta.forEach(([k, v]) => { + if (!v) return; + doc.setFont("helvetica", "bold"); + doc.text(k, margin, y); + doc.setFont("helvetica", "normal"); + doc.text(v, margin + 110, y); + y += 13; + }); + if (legalDescription) { + doc.setFont("helvetica", "bold"); + doc.text("Legal Description:", margin, y); + y += 13; + doc.setFont("helvetica", "normal"); + const ll = doc.splitTextToSize(legalDescription, pageW - margin * 2); + doc.text(ll, margin, y); + y += ll.length * 13; + } + + y += 10; + doc.setFont("helvetica", "bold"); + doc.setFontSize(11); + doc.text("FINANCIAL SUMMARY", margin, y); + y += 16; + doc.setFontSize(10); + const finRows: [string, number][] = [ + ["Balance at Closing", parseFloat(balanceAtClose) || 0], + ["Advance Assessments", parseFloat(advAssessments) || 0], + ["Account Start-up Fee", parseFloat(accountStartupFee) || 0], + ["Delinquency Fee", parseFloat(delinquencyFee) || 0], + ["Estoppel Certificate Fee", parseFloat(estoppelFee) || 0], + ]; + doc.setFont("helvetica", "normal"); + finRows.forEach(([d, a]) => { + doc.text(d, margin, y); + const t = `$${fmtCurrency(a)}`; + doc.text(t, pageW - margin - doc.getTextWidth(t), y); + y += 14; + }); + y += 4; + doc.line(margin, y, pageW - margin, y); + y += 16; + doc.setFont("helvetica", "bold"); + doc.text("GRAND TOTAL DUE AT CLOSING", margin, y); + const gt = `$${fmtCurrency(grandTotal)}`; + doc.text(gt, pageW - margin - doc.getTextWidth(gt), y); + y += 26; + + // Q10 payoff + doc.setFont("helvetica", "bold"); + doc.text("CURRENT PAYOFF (delinquency)", margin, y); + y += 16; + doc.setFont("helvetica", "normal"); + const payoffRows: [string, number][] = [ + ["Unpaid Assessments", parseFloat(q10Unpaid) || 0], + ["Interest", parseFloat(q10Interest) || 0], + ["Late Fees", parseFloat(q10Late) || 0], + ["Admin Fees", parseFloat(q10Admin) || 0], + ["Legal Fees", parseFloat(q10Legal) || 0], + ]; + payoffRows.forEach(([d, a]) => { + doc.text(d, margin, y); + const t = `$${fmtCurrency(a)}`; + doc.text(t, pageW - margin - doc.getTextWidth(t), y); + y += 14; + }); + y += 4; + doc.line(margin, y, pageW - margin, y); + y += 16; + doc.setFont("helvetica", "bold"); + doc.text("PAYOFF SUBTOTAL", margin, y); + const ps = `$${fmtCurrency(q10Sub)}`; + doc.text(ps, pageW - margin - doc.getTextWidth(ps), y); + y += 26; + + // Questionnaire + doc.setFont("helvetica", "bold"); + doc.text("QUESTIONNAIRE", margin, y); + y += 16; + doc.setFont("helvetica", "normal"); + const qa: [string, string][] = [ + ["Account in collections?", q1], + ["Capital contribution due?", q4], + ["Special assessments pending?", q6], + ["Litigation pending?", q8], + ["Open violations?", q17], + ["Foreclosure pending?", q24], + ["Liens recorded?", q26], + ]; + qa.forEach(([q, a]) => { + if (y > 740) { + doc.addPage(); + y = 60; + } + doc.text(q, margin, y); + doc.setFont("helvetica", "bold"); + doc.text(a.toUpperCase(), pageW - margin - 30, y); + doc.setFont("helvetica", "normal"); + y += 14; + }); + + if (additionalInfo) { + y += 10; + doc.setFont("helvetica", "bold"); + doc.text("Additional Information:", margin, y); + y += 14; + doc.setFont("helvetica", "normal"); + const al = doc.splitTextToSize(additionalInfo, pageW - margin * 2); + doc.text(al, margin, y); + } + + doc.save(`Estoppel_${parcelId || "draft"}_${date}.pdf`); + toast.success("Estoppel PDF downloaded"); + }; + + return ( +
+ + + { + setClientId(id); + setClient(c); + }} + onHomeownerChange={() => {}} + showHomeowner={false} + /> +
+
+ + setDate(e.target.value)} /> +
+
+ + setParcelId(e.target.value)} /> +
+
+ + setRecipientName(e.target.value)} /> +
+
+ + setPropertyAddress(e.target.value)} + /> +
+
+ + setSeller(e.target.value)} /> +
+
+ + setBuyerBank(e.target.value)} /> +
+
+
+ +