Changes
Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com>
This commit is contained in:
co-authored by
renee-png
parent
ac26cbdafb
commit
559f9450a0
@@ -15,7 +15,7 @@ import {
|
||||
type HomeownerLite,
|
||||
type FirmInfo,
|
||||
} from "@/lib/forms-shared";
|
||||
import { jsPDF } from "jspdf";
|
||||
import { buildLetterTokens, loadFormTemplate, renderLetterPdf, type FormTemplateConfig } from "@/lib/form-templates";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export function ItfForm() {
|
||||
@@ -24,6 +24,7 @@ export function ItfForm() {
|
||||
const [clientId, setClientId] = useState("");
|
||||
const [homeownerId, setHomeownerId] = useState("");
|
||||
const [firm, setFirm] = useState<FirmInfo | null>(null);
|
||||
const [template, setTemplate] = useState<FormTemplateConfig | null>(null);
|
||||
|
||||
const [letterDate, setLetterDate] = useState(new Date().toISOString().slice(0, 10));
|
||||
const [certifiedNo, setCertifiedNo] = useState("");
|
||||
@@ -37,9 +38,9 @@ export function ItfForm() {
|
||||
|
||||
useEffect(() => {
|
||||
fetchFirm().then(setFirm);
|
||||
loadFormTemplate("itf").then(setTemplate);
|
||||
}, []);
|
||||
|
||||
// 45-day cure window prior to filing foreclosure
|
||||
const dueDate = useMemo(() => {
|
||||
const d = new Date(letterDate + "T12:00:00");
|
||||
d.setDate(d.getDate() + 45);
|
||||
@@ -54,101 +55,56 @@ export function ItfForm() {
|
||||
(parseFloat(lessPayments) || 0);
|
||||
|
||||
const handleExport = () => {
|
||||
if (!client) {
|
||||
if (!client || !template) {
|
||||
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;
|
||||
});
|
||||
const returnAddress = [client.name];
|
||||
if (firm?.company_name) returnAddress.push(`c/o ${firm.company_name}`);
|
||||
returnAddress.push(...clientAddressLines(client));
|
||||
|
||||
doc.setFont("helvetica", "bold");
|
||||
doc.text(fmtDateLong(letterDate), pageW - margin - doc.getTextWidth(fmtDateLong(letterDate)), 60);
|
||||
const recipient: string[] = [ownerFullName(homeowner) || "[Homeowner]"];
|
||||
if (homeowner?.address) recipient.push(homeowner.address);
|
||||
|
||||
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);
|
||||
// Compose lien reference inline so users don't need to edit raw template.
|
||||
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}`],
|
||||
const labels = template.itemLabels ?? {};
|
||||
const items = [
|
||||
{ description: labels.assessments ?? "Assessments", amount: assessments },
|
||||
{ description: labels.interest ?? "Interest", amount: interest },
|
||||
{ description: labels.lateFees ?? "Late Fees", amount: lateFees },
|
||||
{ description: labels.adminFees ?? "Admin / Legal Fees", amount: adminFees },
|
||||
{ description: labels.lessPayments ?? "Less Payments", amount: `-${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);
|
||||
const tokens = buildLetterTokens({
|
||||
clientName: client.name,
|
||||
ownerName: ownerFullName(homeowner),
|
||||
firmName: firm?.company_name ?? "",
|
||||
date: letterDate,
|
||||
dueDate,
|
||||
total,
|
||||
extra: { lienRef },
|
||||
});
|
||||
|
||||
const doc = renderLetterPdf({
|
||||
template,
|
||||
returnAddress,
|
||||
topRightLine: tokens.date as string,
|
||||
recipient,
|
||||
certifiedMailNumber: certifiedNo,
|
||||
tokens,
|
||||
items,
|
||||
total: { label: labels.totalDue ?? "TOTAL DUE", amount: total },
|
||||
filename: `ITF_${ownerFullName(homeowner) || "draft"}_${letterDate}`,
|
||||
});
|
||||
|
||||
doc.save(`ITF_${ownerFullName(homeowner) || "draft"}_${letterDate}.pdf`);
|
||||
toast.success("ITF PDF downloaded");
|
||||
@@ -256,9 +212,15 @@ export function ItfForm() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-[11px] text-muted-foreground">
|
||||
Title, body, fonts, margins, item labels and signature can be customized in{" "}
|
||||
<strong>Settings → Form templates</strong>. Use <code>{`{{lienRef}}`}</code> in the
|
||||
body to insert the recorded-lien reference.
|
||||
</p>
|
||||
|
||||
<div className="flex items-center justify-between pt-2">
|
||||
<span className="text-sm font-medium">Total due: ${fmtCurrency(total)}</span>
|
||||
<Button onClick={handleExport}>
|
||||
<Button onClick={handleExport} disabled={!template}>
|
||||
<FileDown className="h-4 w-4 mr-2" />
|
||||
Export PDF
|
||||
</Button>
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
type HomeownerLite,
|
||||
type FirmInfo,
|
||||
} from "@/lib/forms-shared";
|
||||
import { jsPDF } from "jspdf";
|
||||
import { buildLetterTokens, loadFormTemplate, renderLetterPdf, type FormTemplateConfig } from "@/lib/form-templates";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export function ItlForm() {
|
||||
@@ -24,6 +24,7 @@ export function ItlForm() {
|
||||
const [clientId, setClientId] = useState("");
|
||||
const [homeownerId, setHomeownerId] = useState("");
|
||||
const [firm, setFirm] = useState<FirmInfo | null>(null);
|
||||
const [template, setTemplate] = useState<FormTemplateConfig | null>(null);
|
||||
|
||||
const [letterDate, setLetterDate] = useState(new Date().toISOString().slice(0, 10));
|
||||
const [certifiedNo, setCertifiedNo] = useState("");
|
||||
@@ -35,6 +36,7 @@ export function ItlForm() {
|
||||
|
||||
useEffect(() => {
|
||||
fetchFirm().then(setFirm);
|
||||
loadFormTemplate("itl").then(setTemplate);
|
||||
}, []);
|
||||
|
||||
const dueDate = useMemo(() => {
|
||||
@@ -51,95 +53,47 @@ export function ItlForm() {
|
||||
(parseFloat(lessPayments) || 0);
|
||||
|
||||
const handleExport = () => {
|
||||
if (!client) {
|
||||
if (!client || !template) {
|
||||
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;
|
||||
});
|
||||
const returnAddress = [client.name];
|
||||
if (firm?.company_name) returnAddress.push(`c/o ${firm.company_name}`);
|
||||
returnAddress.push(...clientAddressLines(client));
|
||||
|
||||
doc.setFont("helvetica", "bold");
|
||||
doc.text(fmtDateLong(letterDate), pageW - margin - doc.getTextWidth(fmtDateLong(letterDate)), 60);
|
||||
const recipient: string[] = [ownerFullName(homeowner) || "[Homeowner]"];
|
||||
if (homeowner?.address) recipient.push(homeowner.address);
|
||||
|
||||
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 RECORD A CLAIM OF LIEN";
|
||||
doc.text(title, (pageW - doc.getTextWidth(title)) / 2, y);
|
||||
y += 26;
|
||||
|
||||
doc.setFont("helvetica", "normal");
|
||||
doc.setFontSize(10);
|
||||
const body = `Pursuant to the governing documents of ${client.name} and applicable Florida law, you are hereby notified that the following amounts are past due in connection with the property identified above.\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), a Claim of Lien will be recorded against the subject property and additional collection costs and attorneys' fees will be incurred for which you may be responsible.`;
|
||||
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}`],
|
||||
const labels = template.itemLabels ?? {};
|
||||
const items = [
|
||||
{ description: labels.assessments ?? "Assessments", amount: assessments },
|
||||
{ description: labels.interest ?? "Interest", amount: interest },
|
||||
{ description: labels.lateFees ?? "Late Fees", amount: lateFees },
|
||||
{ description: labels.adminFees ?? "Admin / Legal Fees", amount: adminFees },
|
||||
{ description: labels.lessPayments ?? "Less Payments", amount: `-${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.`;
|
||||
const cl = doc.splitTextToSize(closing, pageW - margin * 2);
|
||||
doc.text(cl, margin, y);
|
||||
const tokens = buildLetterTokens({
|
||||
clientName: client.name,
|
||||
ownerName: ownerFullName(homeowner),
|
||||
firmName: firm?.company_name ?? "",
|
||||
date: letterDate,
|
||||
dueDate,
|
||||
total,
|
||||
});
|
||||
|
||||
const doc = renderLetterPdf({
|
||||
template,
|
||||
returnAddress,
|
||||
topRightLine: tokens.date as string,
|
||||
recipient,
|
||||
certifiedMailNumber: certifiedNo,
|
||||
tokens,
|
||||
items,
|
||||
total: { label: labels.totalDue ?? "TOTAL DUE", amount: total },
|
||||
filename: `ITL_${ownerFullName(homeowner) || "draft"}_${letterDate}`,
|
||||
});
|
||||
|
||||
doc.save(`ITL_${ownerFullName(homeowner) || "draft"}_${letterDate}.pdf`);
|
||||
toast.success("ITL PDF downloaded");
|
||||
@@ -228,9 +182,14 @@ export function ItlForm() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-[11px] text-muted-foreground">
|
||||
Title, body, fonts, margins, item labels and signature can be customized in{" "}
|
||||
<strong>Settings → Form templates</strong>.
|
||||
</p>
|
||||
|
||||
<div className="flex items-center justify-between pt-2">
|
||||
<span className="text-sm font-medium">Total due: ${fmtCurrency(total)}</span>
|
||||
<Button onClick={handleExport}>
|
||||
<Button onClick={handleExport} disabled={!template}>
|
||||
<FileDown className="h-4 w-4 mr-2" />
|
||||
Export PDF
|
||||
</Button>
|
||||
|
||||
@@ -9,13 +9,12 @@ import {
|
||||
clientAddressLines,
|
||||
fetchFirm,
|
||||
fmtCurrency,
|
||||
fmtDateLong,
|
||||
ownerFullName,
|
||||
type ClientLite,
|
||||
type HomeownerLite,
|
||||
type FirmInfo,
|
||||
} from "@/lib/forms-shared";
|
||||
import { jsPDF } from "jspdf";
|
||||
import { buildLetterTokens, loadFormTemplate, renderLetterPdf, type FormTemplateConfig } from "@/lib/form-templates";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface Item {
|
||||
@@ -29,6 +28,7 @@ export function NolaForm() {
|
||||
const [clientId, setClientId] = useState("");
|
||||
const [homeownerId, setHomeownerId] = useState("");
|
||||
const [firm, setFirm] = useState<FirmInfo | null>(null);
|
||||
const [template, setTemplate] = useState<FormTemplateConfig | null>(null);
|
||||
|
||||
const [date, setDate] = useState(new Date().toISOString().slice(0, 10));
|
||||
const [dueDate, setDueDate] = useState(() => {
|
||||
@@ -37,14 +37,19 @@ export function NolaForm() {
|
||||
return d.toISOString().slice(0, 10);
|
||||
});
|
||||
const [certifiedNo, setCertifiedNo] = useState("");
|
||||
const [items, setItems] = useState<Item[]>([
|
||||
{ description: "Assessments", amount: "0.00" },
|
||||
{ description: "Late Fees", amount: "0.00" },
|
||||
{ description: "Interest", amount: "0.00" },
|
||||
]);
|
||||
const [items, setItems] = useState<Item[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchFirm().then(setFirm);
|
||||
loadFormTemplate("nola").then((t) => {
|
||||
setTemplate(t);
|
||||
setItems(
|
||||
(t.defaultLineItems ?? []).map((it) => ({
|
||||
description: it.description,
|
||||
amount: it.amount,
|
||||
})),
|
||||
);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const total = items.reduce((s, it) => s + (parseFloat(it.amount) || 0), 0);
|
||||
@@ -56,89 +61,38 @@ export function NolaForm() {
|
||||
const removeItem = (i: number) => setItems((p) => p.filter((_, idx) => idx !== i));
|
||||
|
||||
const handleExport = () => {
|
||||
if (!client) {
|
||||
if (!client || !template) {
|
||||
toast.error("Select a client first");
|
||||
return;
|
||||
}
|
||||
const doc = new jsPDF({ unit: "pt", format: "letter" });
|
||||
const pageW = doc.internal.pageSize.getWidth();
|
||||
const margin = 54;
|
||||
|
||||
// Header — return address
|
||||
doc.setFont("helvetica", "normal");
|
||||
doc.setFontSize(10);
|
||||
let y = 60;
|
||||
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;
|
||||
const returnAddress = [client.name];
|
||||
if (firm?.company_name) returnAddress.push(`c/o ${firm.company_name}`);
|
||||
returnAddress.push(...clientAddressLines(client));
|
||||
|
||||
const recipient: string[] = [ownerFullName(homeowner) || "[Homeowner]"];
|
||||
if (homeowner?.address) recipient.push(homeowner.address);
|
||||
|
||||
const tokens = buildLetterTokens({
|
||||
clientName: client.name,
|
||||
ownerName: ownerFullName(homeowner),
|
||||
firmName: firm?.company_name ?? "",
|
||||
date,
|
||||
dueDate,
|
||||
total,
|
||||
});
|
||||
|
||||
// Date right
|
||||
doc.setFont("helvetica", "bold");
|
||||
doc.text(fmtDateLong(date), pageW - margin - doc.getTextWidth(fmtDateLong(date)), 60);
|
||||
|
||||
// Recipient
|
||||
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;
|
||||
}
|
||||
|
||||
// Certified mail
|
||||
if (certifiedNo) {
|
||||
doc.setFont("helvetica", "bold");
|
||||
doc.setFontSize(9);
|
||||
const txt = `U.S. Certified Mail #: ${certifiedNo}`;
|
||||
doc.text(txt, pageW - margin - doc.getTextWidth(txt), y);
|
||||
y += 14;
|
||||
}
|
||||
|
||||
// Title
|
||||
y += 30;
|
||||
doc.setFont("helvetica", "bold");
|
||||
doc.setFontSize(13);
|
||||
const title = "NOTICE OF LATE ASSESSMENT";
|
||||
doc.text(title, (pageW - doc.getTextWidth(title)) / 2, y);
|
||||
y += 30;
|
||||
|
||||
// Body
|
||||
doc.setFont("helvetica", "normal");
|
||||
doc.setFontSize(10);
|
||||
const body = `This letter serves as formal notice that your account with ${client.name} is delinquent. The amounts owed as of ${fmtDateLong(date)} are itemized below. Payment in full must be received on or before ${fmtDateLong(dueDate)} to avoid further collection action, including the filing of a Notice of Intent to Lien.`;
|
||||
const bodyLines = doc.splitTextToSize(body, pageW - margin * 2);
|
||||
doc.text(bodyLines, margin, y);
|
||||
y += bodyLines.length * 13 + 16;
|
||||
|
||||
// Items table
|
||||
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");
|
||||
items.forEach((it) => {
|
||||
doc.text(it.description || "—", margin, y);
|
||||
const amt = `$${fmtCurrency(it.amount)}`;
|
||||
doc.text(amt, pageW - margin - doc.getTextWidth(amt), y);
|
||||
y += 14;
|
||||
const doc = renderLetterPdf({
|
||||
template,
|
||||
returnAddress,
|
||||
topRightLine: tokens.date as string,
|
||||
recipient,
|
||||
certifiedMailNumber: certifiedNo,
|
||||
tokens,
|
||||
items: items.map((it) => ({ description: it.description, amount: it.amount })),
|
||||
total: { label: template.itemLabels?.totalDue ?? "TOTAL DUE", amount: total },
|
||||
filename: `NOLA_${ownerFullName(homeowner) || "draft"}_${date}`,
|
||||
});
|
||||
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);
|
||||
|
||||
doc.save(`NOLA_${ownerFullName(homeowner) || "draft"}_${date}.pdf`);
|
||||
toast.success("NOLA PDF downloaded");
|
||||
@@ -206,8 +160,13 @@ export function NolaForm() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-[11px] text-muted-foreground">
|
||||
Title, body, fonts, margins, and signature can be customized in{" "}
|
||||
<strong>Settings → Form templates</strong>.
|
||||
</p>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button onClick={handleExport}>
|
||||
<Button onClick={handleExport} disabled={!template}>
|
||||
<FileDown className="h-4 w-4 mr-2" />
|
||||
Export PDF
|
||||
</Button>
|
||||
|
||||
Reference in New Issue
Block a user