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() { - +