diff --git a/src/components/forms/estoppel-form.tsx b/src/components/forms/estoppel-form.tsx index 7061c6b..a2c7d62 100644 --- a/src/components/forms/estoppel-form.tsx +++ b/src/components/forms/estoppel-form.tsx @@ -135,11 +135,11 @@ export function EstoppelForm() { doc.text(`c/o ${firm.company_name}`, margin, y); y += 12; } - doc.setFont("helvetica", "bold"); + doc.setFont(font, "bold"); doc.text(fmtDateLong(date), pageW - margin - doc.getTextWidth(fmtDateLong(date)), 60); y += 16; - doc.setFont("helvetica", "normal"); + doc.setFont(font, "normal"); const recipientLines = [ `TO: ${recipientName}`, ...recipientAddress.split("\n").filter(Boolean), @@ -158,24 +158,24 @@ export function EstoppelForm() { ]; meta.forEach(([k, v]) => { if (!v) return; - doc.setFont("helvetica", "bold"); + doc.setFont(font, "bold"); doc.text(k, margin, y); - doc.setFont("helvetica", "normal"); + doc.setFont(font, "normal"); doc.text(v, margin + 110, y); y += 13; }); if (legalDescription) { - doc.setFont("helvetica", "bold"); + doc.setFont(font, "bold"); doc.text("Legal Description:", margin, y); y += 13; - doc.setFont("helvetica", "normal"); + doc.setFont(font, "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.setFont(font, "bold"); doc.setFontSize(11); doc.text("FINANCIAL SUMMARY", margin, y); y += 16; @@ -187,7 +187,7 @@ export function EstoppelForm() { ["Delinquency Fee", parseFloat(delinquencyFee) || 0], ["Estoppel Certificate Fee", parseFloat(estoppelFee) || 0], ]; - doc.setFont("helvetica", "normal"); + doc.setFont(font, "normal"); finRows.forEach(([d, a]) => { doc.text(d, margin, y); const t = `$${fmtCurrency(a)}`; @@ -197,17 +197,17 @@ export function EstoppelForm() { y += 4; doc.line(margin, y, pageW - margin, y); y += 16; - doc.setFont("helvetica", "bold"); + doc.setFont(font, "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.setFont(font, "bold"); doc.text("CURRENT PAYOFF (delinquency)", margin, y); y += 16; - doc.setFont("helvetica", "normal"); + doc.setFont(font, "normal"); const payoffRows: [string, number][] = [ ["Unpaid Assessments", parseFloat(q10Unpaid) || 0], ["Interest", parseFloat(q10Interest) || 0], @@ -224,17 +224,17 @@ export function EstoppelForm() { y += 4; doc.line(margin, y, pageW - margin, y); y += 16; - doc.setFont("helvetica", "bold"); + doc.setFont(font, "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.setFont(font, "bold"); doc.text("QUESTIONNAIRE", margin, y); y += 16; - doc.setFont("helvetica", "normal"); + doc.setFont(font, "normal"); const qa: [string, string][] = [ ["Account in collections?", q1], ["Capital contribution due?", q4], @@ -250,18 +250,18 @@ export function EstoppelForm() { y = 60; } doc.text(q, margin, y); - doc.setFont("helvetica", "bold"); + doc.setFont(font, "bold"); doc.text(a.toUpperCase(), pageW - margin - 30, y); - doc.setFont("helvetica", "normal"); + doc.setFont(font, "normal"); y += 14; }); if (additionalInfo) { y += 10; - doc.setFont("helvetica", "bold"); + doc.setFont(font, "bold"); doc.text("Additional Information:", margin, y); y += 14; - doc.setFont("helvetica", "normal"); + doc.setFont(font, "normal"); const al = doc.splitTextToSize(additionalInfo, pageW - margin * 2); doc.text(al, margin, y); } diff --git a/src/routes/settings.form-templates.tsx b/src/routes/settings.form-templates.tsx new file mode 100644 index 0000000..c80a139 --- /dev/null +++ b/src/routes/settings.form-templates.tsx @@ -0,0 +1,297 @@ +import { createFileRoute, redirect } from "@tanstack/react-router"; +import { useEffect, useState } from "react"; +import { useAuth } from "@/lib/auth"; +import { Card, CardContent } from "@/components/ui/card"; +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 { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"; +import { Loader2, RotateCcw, Save } from "lucide-react"; +import { toast } from "sonner"; +import { + DEFAULT_TEMPLATES, + FORM_LABELS, + loadFormTemplate, + saveFormTemplate, + type FormKey, + type FormTemplateConfig, +} from "@/lib/form-templates"; + +export const Route = createFileRoute("/settings/form-templates")({ + component: FormTemplatesPage, +}); + +const KEYS: FormKey[] = ["nola", "itl", "itf", "estoppel", "letter"]; + +function FormTemplatesPage() { + const { isAdmin, user, loading: authLoading } = useAuth(); + const [tab, setTab] = useState("nola"); + + if (!authLoading && !isAdmin) { + throw redirect({ to: "/settings" }); + } + + return ( +
+
+

Form templates

+

+ Edit the title, body text, fonts, margins, item labels and signature block used by each + built-in form. Changes apply the next time anyone generates a PDF for that form. +

+
+ setTab(v as FormKey)}> + + {KEYS.map((k) => ( + + {FORM_LABELS[k]} + + ))} + + {KEYS.map((k) => ( + + + + ))} + +
+ ); +} + +function TemplateEditor({ formKey, userId }: { formKey: FormKey; userId: string | null }) { + const [config, setConfig] = useState(null); + const [saving, setSaving] = useState(false); + + useEffect(() => { + setConfig(null); + loadFormTemplate(formKey).then(setConfig); + }, [formKey]); + + if (!config) { + return ( +
+ Loading template… +
+ ); + } + + const update = (key: K, value: FormTemplateConfig[K]) => + setConfig({ ...config, [key]: value }); + + const updateLabel = ( + key: keyof NonNullable, + value: string, + ) => setConfig({ ...config, itemLabels: { ...(config.itemLabels ?? {}), [key]: value } }); + + const handleSave = async () => { + setSaving(true); + try { + await saveFormTemplate(formKey, config, userId); + toast.success("Template saved"); + } catch (e: any) { + toast.error(e.message ?? "Save failed"); + } finally { + setSaving(false); + } + }; + + const handleReset = () => { + if (!confirm("Reset this template to the built-in defaults? Unsaved changes will be lost.")) return; + setConfig({ ...DEFAULT_TEMPLATES[formKey] }); + }; + + const showItemLabels = ["itl", "itf", "estoppel"].includes(formKey); + const showDefaultLineItems = formKey === "nola"; + + return ( + + +
+
+ + update("title", e.target.value)} + placeholder="e.g. NOTICE OF LATE ASSESSMENT" + /> +
+
+ + update("certifiedMailLabel", e.target.value)} + placeholder="U.S. Certified Mail #" + /> +
+
+ +
+ +