Added service list picker

X-Lovable-Edit-ID: edt-dcc30b86-7a14-4f71-b824-c6fed2507ada
Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com>
This commit is contained in:
gpt-engineer-app[bot]
2026-04-18 18:47:36 +00:00
co-authored by renee-png
3 changed files with 209 additions and 3 deletions
+37
View File
@@ -36,6 +36,15 @@ export interface PleadingSignature {
imageType?: "png" | "jpg" | "jpeg";
}
export interface ServiceContact {
name: string;
role?: string;
company?: string;
email?: string;
phone?: string;
address?: string; // multi-line
}
export interface PleadingInput {
courtType: "CIRCUIT" | "COUNTY";
circuit: string; // e.g. "ELEVENTH"
@@ -51,6 +60,8 @@ export interface PleadingInput {
footerCenter?: string;
footerRight?: string; // user text prepended to "Page X of Y"
signature?: PleadingSignature;
serviceList?: ServiceContact[];
serviceListTitle?: string;
}
const FONT = "Bookman Old Style";
@@ -375,6 +386,32 @@ export function buildPleadingDoc(input: PleadingInput): Document {
});
}
// Service list (optional) — rendered on a fresh page at end
const serviceList = (input.serviceList || []).filter((c) => c.name.trim().length > 0);
if (serviceList.length > 0) {
const slTitle = (input.serviceListTitle || "SERVICE LIST").toUpperCase();
bodyParagraphs.push(
new Paragraph({
pageBreakBefore: true,
alignment: AlignmentType.CENTER,
children: [run(slTitle, { bold: true })],
}),
);
bodyParagraphs.push(emptyP());
serviceList.forEach((c, idx) => {
bodyParagraphs.push(p(c.name, { bold: true }));
if (c.role || c.company) {
bodyParagraphs.push(p([c.role, c.company].filter(Boolean).join(", ")));
}
if (c.address) {
c.address.split("\n").forEach((line) => bodyParagraphs.push(p(line)));
}
if (c.phone) bodyParagraphs.push(p(`Tel: ${c.phone}`));
if (c.email) bodyParagraphs.push(p(`Email: ${c.email}`));
if (idx < serviceList.length - 1) bodyParagraphs.push(emptyP());
});
}
return new Document({
styles: {
default: { document: { run: { font: FONT, size: SIZE } } },
+28
View File
@@ -328,6 +328,34 @@ export async function downloadPleadingPdf(input: PleadingInput, filename: string
}
}
// Service list — fresh page at end
const serviceList = (input.serviceList || []).filter((c) => c.name.trim().length > 0);
if (serviceList.length > 0) {
pdf.addPage();
y = MARGIN;
const slTitle = (input.serviceListTitle || "SERVICE LIST").toUpperCase();
setFont(pdf, { bold: true });
pdf.text(slTitle, PAGE_W / 2, y, { align: "center" });
y += LINE_H * 2;
setFont(pdf, {});
serviceList.forEach((c, idx) => {
const block: string[] = [];
block.push(c.name);
const sub = [c.role, c.company].filter(Boolean).join(", ");
if (sub) block.push(sub);
if (c.address) c.address.split("\n").forEach((l) => block.push(l));
if (c.phone) block.push(`Tel: ${c.phone}`);
if (c.email) block.push(`Email: ${c.email}`);
block.forEach((line, i) => {
ensureSpace(LINE_H);
setFont(pdf, { bold: i === 0 });
pdf.text(line, MARGIN, y);
y += LINE_H;
});
if (idx < serviceList.length - 1) y += LINE_H * 0.5;
});
}
const total = pdf.getNumberOfPages();
for (let i = 1; i <= total; i++) {
pdf.setPage(i);
+144 -3
View File
@@ -13,13 +13,14 @@ import {
import { Switch } from "@/components/ui/switch";
import { FL_CIRCUITS } from "@/lib/florida";
import { Packer } from "docx";
import { buildPleadingDoc, downloadPleading, type PleadingFootnote, type PleadingSignature } from "@/lib/docx-pleading";
import { buildPleadingDoc, downloadPleading, type PleadingFootnote, type PleadingSignature, type ServiceContact } from "@/lib/docx-pleading";
import { downloadPleadingPdf } from "@/lib/pdf-pleading";
import { RichTextEditor } from "@/components/documents/rich-text-editor";
import { ContactPickerPopover } from "@/components/clients/contact-picker-popover";
import { supabase } from "@/integrations/supabase/client";
import { useAuth } from "@/lib/auth";
import { toast } from "sonner";
import { Download, FileText, Save, Loader2, Plus, Trash2, PenLine } from "lucide-react";
import { Download, FileText, Save, Loader2, Plus, Trash2, PenLine, Users } from "lucide-react";
export const Route = createFileRoute("/documents/pleading/new")({
component: PleadingNewPage,
@@ -46,6 +47,8 @@ function PleadingNewPage() {
const [footerRight, setFooterRight] = useState("");
const [docName, setDocName] = useState("Pleading");
const [saving, setSaving] = useState(false);
const [serviceList, setServiceList] = useState<ServiceContact[]>([]);
const [serviceListTitle, setServiceListTitle] = useState("SERVICE LIST");
// Attorney signature (loaded from profile)
const [includeSignature, setIncludeSignature] = useState(true);
@@ -124,6 +127,8 @@ function PleadingNewPage() {
if (typeof p.footerLeft === "string") setFooterLeft(p.footerLeft);
if (typeof p.footerCenter === "string") setFooterCenter(p.footerCenter);
if (typeof p.footerRight === "string") setFooterRight(p.footerRight);
if (Array.isArray(p.serviceList)) setServiceList(p.serviceList);
if (typeof p.serviceListTitle === "string") setServiceListTitle(p.serviceListTitle);
setDocName(`${data.name} (copy)`);
toast.success("Loaded saved pleading");
})();
@@ -146,6 +151,8 @@ function PleadingNewPage() {
title, bodyHtml, footnotes,
footerLeft, footerCenter, footerRight,
signature: buildSignature(),
serviceList,
serviceListTitle,
};
const headerLine1 = `IN THE ${courtType} COURT OF THE ${circuit} JUDICIAL CIRCUIT,`;
@@ -437,7 +444,141 @@ function PleadingNewPage() {
</CardContent>
</Card>
{/* Footer */}
{/* Service list (optional) */}
<Card className="mt-6">
<CardContent className="p-5 space-y-4">
<div className="flex items-start justify-between gap-4">
<div>
<Label className="text-base flex items-center gap-2">
<Users className="h-4 w-4 text-muted-foreground" /> Service list
</Label>
<p className="text-xs text-muted-foreground mt-0.5">
Optional. If you add any contacts, a dedicated page is appended to the pleading listing each one.
</p>
</div>
<ContactPickerPopover
label="Add from contacts"
onPick={(c) => {
setServiceList((prev) => [
...prev,
{
name: c.name,
role: c.title || undefined,
company: c.company || undefined,
email: c.email || undefined,
phone: c.phone || undefined,
address: "",
},
]);
}}
/>
</div>
<div className="space-y-1.5 max-w-sm">
<Label className="text-xs">Page title</Label>
<Input
value={serviceListTitle}
onChange={(e) => setServiceListTitle(e.target.value)}
placeholder="SERVICE LIST"
/>
</div>
{serviceList.length === 0 ? (
<div className="text-sm text-muted-foreground italic py-4 text-center border border-dashed rounded">
No contacts added. Use “Add from contacts” to include a service list page.
</div>
) : (
<div className="space-y-3">
{serviceList.map((c, idx) => (
<div key={idx} className="border rounded-md p-3 space-y-2 bg-muted/10">
<div className="flex items-start justify-between gap-2">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2 flex-1">
<Input
placeholder="Name"
value={c.name}
onChange={(e) =>
setServiceList((prev) =>
prev.map((x, i) => (i === idx ? { ...x, name: e.target.value } : x)),
)
}
/>
<Input
placeholder="Role / title"
value={c.role || ""}
onChange={(e) =>
setServiceList((prev) =>
prev.map((x, i) => (i === idx ? { ...x, role: e.target.value } : x)),
)
}
/>
<Input
placeholder="Company / firm"
value={c.company || ""}
onChange={(e) =>
setServiceList((prev) =>
prev.map((x, i) => (i === idx ? { ...x, company: e.target.value } : x)),
)
}
/>
<Input
placeholder="Email"
value={c.email || ""}
onChange={(e) =>
setServiceList((prev) =>
prev.map((x, i) => (i === idx ? { ...x, email: e.target.value } : x)),
)
}
/>
<Input
placeholder="Phone"
value={c.phone || ""}
onChange={(e) =>
setServiceList((prev) =>
prev.map((x, i) => (i === idx ? { ...x, phone: e.target.value } : x)),
)
}
/>
<Textarea
rows={2}
placeholder="Address (one line per row)"
value={c.address || ""}
onChange={(e) =>
setServiceList((prev) =>
prev.map((x, i) => (i === idx ? { ...x, address: e.target.value } : x)),
)
}
/>
</div>
<Button
variant="ghost"
size="icon"
onClick={() =>
setServiceList((prev) => prev.filter((_, i) => i !== idx))
}
title="Remove"
>
<Trash2 className="h-4 w-4 text-destructive" />
</Button>
</div>
</div>
))}
<Button
variant="outline"
size="sm"
onClick={() =>
setServiceList((prev) => [
...prev,
{ name: "", role: "", company: "", email: "", phone: "", address: "" },
])
}
>
<Plus className="h-4 w-4 mr-1" /> Add blank entry
</Button>
</div>
)}
</CardContent>
</Card>
<Card className="mt-6">
<CardContent className="p-5 space-y-3">
<div>