From 4b36efc3fa88fd56d780220e751ae51fea23efae Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Sat, 18 Apr 2026 02:40:36 +0000 Subject: [PATCH] Changes Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com> --- src/components/forms/affidavit-form.tsx | 248 ---------------------- src/components/forms/itf-form.tsx | 270 ++++++++++++++++++++++++ src/routes/forms.index.tsx | 6 +- 3 files changed, 273 insertions(+), 251 deletions(-) delete mode 100644 src/components/forms/affidavit-form.tsx create mode 100644 src/components/forms/itf-form.tsx diff --git a/src/components/forms/affidavit-form.tsx b/src/components/forms/affidavit-form.tsx deleted file mode 100644 index efe7f54..0000000 --- a/src/components/forms/affidavit-form.tsx +++ /dev/null @@ -1,248 +0,0 @@ -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/itf-form.tsx b/src/components/forms/itf-form.tsx new file mode 100644 index 0000000..44323fc --- /dev/null +++ b/src/components/forms/itf-form.tsx @@ -0,0 +1,270 @@ +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 { Card, CardContent } from "@/components/ui/card"; +import { FileDown } from "lucide-react"; +import { ClientHomeownerPicker } from "./form-pickers"; +import { + clientAddressLines, + fetchFirm, + fmtCurrency, + fmtDateLong, + ownerFullName, + type ClientLite, + type HomeownerLite, + type FirmInfo, +} from "@/lib/forms-shared"; +import { jsPDF } from "jspdf"; +import { toast } from "sonner"; + +export function ItfForm() { + const [client, setClient] = useState(null); + const [homeowner, setHomeowner] = useState(null); + const [clientId, setClientId] = useState(""); + const [homeownerId, setHomeownerId] = useState(""); + const [firm, setFirm] = useState(null); + + const [letterDate, setLetterDate] = useState(new Date().toISOString().slice(0, 10)); + const [certifiedNo, setCertifiedNo] = useState(""); + const [lienBookPage, setLienBookPage] = useState(""); + const [lienRecordedDate, setLienRecordedDate] = useState(""); + const [assessments, setAssessments] = useState("0.00"); + const [interest, setInterest] = useState("0.00"); + const [lateFees, setLateFees] = useState("0.00"); + const [adminFees, setAdminFees] = useState("0.00"); + const [lessPayments, setLessPayments] = useState("0.00"); + + useEffect(() => { + fetchFirm().then(setFirm); + }, []); + + // 45-day cure window prior to filing foreclosure + const dueDate = useMemo(() => { + const d = new Date(letterDate + "T12:00:00"); + d.setDate(d.getDate() + 45); + return d.toISOString().slice(0, 10); + }, [letterDate]); + + const total = + (parseFloat(assessments) || 0) + + (parseFloat(interest) || 0) + + (parseFloat(lateFees) || 0) + + (parseFloat(adminFees) || 0) - + (parseFloat(lessPayments) || 0); + + 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; + 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; + } + clientAddressLines(client).forEach((l) => { + doc.text(l, margin, y); + y += 12; + }); + + doc.setFont("helvetica", "bold"); + doc.text(fmtDateLong(letterDate), pageW - margin - doc.getTextWidth(fmtDateLong(letterDate)), 60); + + y = Math.max(y + 30, 175); + doc.setFont("helvetica", "normal"); + doc.text(ownerFullName(homeowner) || "[Homeowner]", margin, y); + y += 12; + if (homeowner?.address) { + doc.text(homeowner.address, margin, y); + y += 12; + } + + if (certifiedNo) { + doc.setFont("helvetica", "bolditalic"); + const t = `U.S. Certified Mail # ${certifiedNo}`; + doc.text(t, pageW - margin - doc.getTextWidth(t), y); + y += 16; + } + + y += 24; + doc.setFont("helvetica", "bold"); + doc.setFontSize(12); + const title = "NOTICE OF INTENT TO FORECLOSE CLAIM OF LIEN"; + doc.text(title, (pageW - doc.getTextWidth(title)) / 2, y); + y += 26; + + doc.setFont("helvetica", "normal"); + doc.setFontSize(10); + const lienRef = + lienBookPage || lienRecordedDate + ? ` A Claim of Lien was recorded against the subject property${ + lienRecordedDate ? ` on ${fmtDateLong(lienRecordedDate)}` : "" + }${lienBookPage ? ` (recorded at ${lienBookPage})` : ""}.` + : ""; + const body = `Pursuant to the governing documents of ${client.name} and applicable Florida law, you are hereby notified that the assessments and other amounts secured by the previously recorded Claim of Lien against the property identified above remain past due and unpaid.${lienRef}\n\nUnless payment in full of the total amount stated below is received on or before ${fmtDateLong(dueDate)} (forty-five (45) days from the date of this letter), the Association will proceed with the filing of a foreclosure action on its Claim of Lien. Additional collection costs and attorneys' fees will be incurred for which you may be responsible, and your interest in the property may be sold at public sale.`; + const lines = doc.splitTextToSize(body, pageW - margin * 2); + doc.text(lines, margin, y); + y += lines.length * 13 + 14; + + // Itemized + const rows: [string, string][] = [ + ["Assessments", assessments], + ["Interest", interest], + ["Late Fees", lateFees], + ["Admin / Legal Fees", adminFees], + ["Less Payments", `-${lessPayments}`], + ]; + doc.setFont("helvetica", "bold"); + doc.text("Description", margin, y); + doc.text("Amount", pageW - margin - 60, y); + y += 6; + doc.line(margin, y, pageW - margin, y); + y += 14; + doc.setFont("helvetica", "normal"); + rows.forEach(([d, a]) => { + doc.text(d, margin, y); + const txt = `$${fmtCurrency(a)}`; + doc.text(txt, pageW - margin - doc.getTextWidth(txt), y); + y += 14; + }); + y += 4; + doc.line(margin, y, pageW - margin, y); + y += 16; + doc.setFont("helvetica", "bold"); + doc.text("TOTAL DUE", margin, y); + const totalTxt = `$${fmtCurrency(total)}`; + doc.text(totalTxt, pageW - margin - doc.getTextWidth(totalTxt), y); + y += 30; + + doc.setFont("helvetica", "normal"); + const closing = `Payment must be made payable to ${client.name} and remitted to the address shown above. If you have any questions regarding this notice, please contact our office immediately to avoid the filing of a foreclosure action.`; + const cl = doc.splitTextToSize(closing, pageW - margin * 2); + doc.text(cl, margin, y); + + doc.save(`ITF_${ownerFullName(homeowner) || "draft"}_${letterDate}.pdf`); + toast.success("ITF PDF downloaded"); + }; + + return ( +
+ + + { + setClientId(id); + setClient(c); + }} + onHomeownerChange={(id, h) => { + setHomeownerId(id); + setHomeowner(h); + }} + /> +
+
+ + setLetterDate(e.target.value)} + /> +
+
+ + +
+
+ + setCertifiedNo(e.target.value)} /> +
+
+ +
+
+ + setLienRecordedDate(e.target.value)} + /> +
+
+ + setLienBookPage(e.target.value)} + placeholder="e.g., Book 1234 / Page 567 or Inst # 2024000123" + /> +
+
+ +
+
+ + setAssessments(e.target.value)} + /> +
+
+ + setInterest(e.target.value)} + /> +
+
+ + setLateFees(e.target.value)} + /> +
+
+ + setAdminFees(e.target.value)} + /> +
+
+ + setLessPayments(e.target.value)} + /> +
+
+ +
+ Total due: ${fmtCurrency(total)} + +
+
+
+
+ ); +} diff --git a/src/routes/forms.index.tsx b/src/routes/forms.index.tsx index e4a3137..702188a 100644 --- a/src/routes/forms.index.tsx +++ b/src/routes/forms.index.tsx @@ -7,7 +7,7 @@ import { CustomFormBuilder } from "@/components/forms/custom-form-builder"; import { LetterGenerator } from "@/components/forms/letter-generator"; import { NolaForm } from "@/components/forms/nola-form"; import { ItlForm } from "@/components/forms/itl-form"; -import { AffidavitForm } from "@/components/forms/affidavit-form"; +import { ItfForm } from "@/components/forms/itf-form"; import { EstoppelForm } from "@/components/forms/estoppel-form"; export const Route = createFileRoute("/forms/")({ @@ -29,7 +29,7 @@ function FormsPage() { Letters NOLA ITL - Affidavit + ITF Estoppel
@@ -37,7 +37,7 @@ function FormsPage() { - +