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:
@@ -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 } } },
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user