diff --git a/src/lib/docx-pleading.ts b/src/lib/docx-pleading.ts new file mode 100644 index 0000000..b4fd212 --- /dev/null +++ b/src/lib/docx-pleading.ts @@ -0,0 +1,194 @@ +import { + AlignmentType, + Document, + HeightRule, + Packer, + PageOrientation, + Paragraph, + Table, + TableCell, + TableRow, + TextRun, + WidthType, + BorderStyle, +} from "docx"; +import { saveAs } from "file-saver"; + +export interface PleadingInput { + courtType: "CIRCUIT" | "COUNTY"; + circuit: string; // e.g. "ELEVENTH" + county: string; // e.g. "Miami-Dade" + plaintiffs: string; // multi-line + defendants: string; // multi-line + caseNumber: string; + title?: string; + body?: string; +} + +const FONT = "Bookman Old Style"; +const SIZE = 24; // 12pt (docx uses half-points) + +const noBorder = { + top: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" }, + bottom: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" }, + left: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" }, + right: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" }, + insideHorizontal: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" }, + insideVertical: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" }, +}; + +const verticalLine = { + top: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" }, + bottom: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" }, + left: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" }, + right: { style: BorderStyle.SINGLE, size: 8, color: "000000" }, +}; + +function run(text: string, opts: { bold?: boolean } = {}) { + return new TextRun({ text, bold: opts.bold, font: FONT, size: SIZE }); +} + +function p(text: string, opts: { bold?: boolean; align?: (typeof AlignmentType)[keyof typeof AlignmentType] } = {}) { + return new Paragraph({ + alignment: opts.align, + children: [run(text, { bold: opts.bold })], + }); +} + +function emptyP() { + return new Paragraph({ children: [run("")] }); +} + +export function buildPleadingDoc(input: PleadingInput): Document { + const headerLine1 = `IN THE ${input.courtType} COURT OF THE ${input.circuit} JUDICIAL CIRCUIT,`; + const headerLine2 = `IN AND FOR ${input.county.toUpperCase()} COUNTY, FLORIDA`; + + const plaintiffLines = (input.plaintiffs || "").split("\n").filter((l) => l.trim().length > 0); + const defendantLines = (input.defendants || "").split("\n").filter((l) => l.trim().length > 0); + + // Left side of caption + const leftCells: Paragraph[] = []; + plaintiffLines.forEach((l) => leftCells.push(p(l))); + if (plaintiffLines.length === 0) leftCells.push(p("")); + leftCells.push(emptyP()); + leftCells.push(p("Plaintiff(s),")); + leftCells.push(emptyP()); + leftCells.push(p("v.")); + leftCells.push(emptyP()); + defendantLines.forEach((l) => leftCells.push(p(l))); + if (defendantLines.length === 0) leftCells.push(p("")); + leftCells.push(emptyP()); + leftCells.push(p("Defendant(s).")); + + const rightCells: Paragraph[] = [ + p(`CASE NO.: ${input.caseNumber || ""}`, { bold: true }), + ]; + + const captionTable = new Table({ + width: { size: 9360, type: WidthType.DXA }, + columnWidths: [5400, 3960], + rows: [ + new TableRow({ + height: { value: 400, rule: HeightRule.AUTO }, + children: [ + new TableCell({ + width: { size: 5400, type: WidthType.DXA }, + borders: verticalLine, + margins: { top: 80, bottom: 80, left: 0, right: 200 }, + children: leftCells, + }), + new TableCell({ + width: { size: 3960, type: WidthType.DXA }, + borders: noBorder, + margins: { top: 80, bottom: 80, left: 200, right: 0 }, + children: rightCells, + }), + ], + }), + ], + }); + + const bodyParagraphs: Paragraph[] = []; + if (input.title) { + bodyParagraphs.push(emptyP()); + bodyParagraphs.push(p(input.title.toUpperCase(), { bold: true, align: AlignmentType.CENTER })); + bodyParagraphs.push(emptyP()); + } + if (input.body) { + input.body.split("\n").forEach((line) => bodyParagraphs.push(p(line))); + } + + return new Document({ + styles: { + default: { document: { run: { font: FONT, size: SIZE } } }, + }, + sections: [ + { + properties: { + page: { + size: { width: 12240, height: 15840, orientation: PageOrientation.PORTRAIT }, + margin: { top: 1440, right: 1440, bottom: 1440, left: 1440 }, + }, + }, + children: [ + p(headerLine1, { bold: true, align: AlignmentType.CENTER }), + p(headerLine2, { bold: true, align: AlignmentType.CENTER }), + emptyP(), + captionTable, + ...bodyParagraphs, + ], + }, + ], + }); +} + +export async function downloadPleading(input: PleadingInput, filename: string) { + const doc = buildPleadingDoc(input); + const blob = await Packer.toBlob(doc); + saveAs(blob, filename.endsWith(".docx") ? filename : `${filename}.docx`); +} + +// ---------- Generic template (custom fields) ---------- + +export interface GenericDocInput { + title?: string; + body: string; // Already merged (fields substituted) + fontFamily?: string; + fontSizePt?: number; +} + +export async function downloadGeneric(input: GenericDocInput, filename: string) { + const font = input.fontFamily || "Bookman Old Style"; + const size = (input.fontSizePt || 12) * 2; + + const mkP = (text: string, bold = false, center = false) => + new Paragraph({ + alignment: center ? AlignmentType.CENTER : undefined, + children: [new TextRun({ text, bold, font, size })], + }); + + const children: Paragraph[] = []; + if (input.title) { + children.push(mkP(input.title, true, true)); + children.push(new Paragraph({ children: [new TextRun({ text: "", font, size })] })); + } + input.body.split("\n").forEach((line) => children.push(mkP(line))); + + const doc = new Document({ + styles: { default: { document: { run: { font, size } } } }, + sections: [ + { + properties: { + page: { + size: { width: 12240, height: 15840 }, + margin: { top: 1440, right: 1440, bottom: 1440, left: 1440 }, + }, + }, + children, + }, + ], + }); + + const blob = await Packer.toBlob(doc); + saveAs(blob, filename.endsWith(".docx") ? filename : `${filename}.docx`); +} diff --git a/src/lib/florida.ts b/src/lib/florida.ts new file mode 100644 index 0000000..a1b1f30 --- /dev/null +++ b/src/lib/florida.ts @@ -0,0 +1,27 @@ +// Florida judicial circuits (1st–20th) and the counties they cover. +export const FL_CIRCUITS: { value: string; label: string; counties: string[] }[] = [ + { value: "FIRST", label: "First", counties: ["Escambia", "Okaloosa", "Santa Rosa", "Walton"] }, + { value: "SECOND", label: "Second", counties: ["Franklin", "Gadsden", "Jefferson", "Leon", "Liberty", "Wakulla"] }, + { value: "THIRD", label: "Third", counties: ["Columbia", "Dixie", "Hamilton", "Lafayette", "Madison", "Suwannee", "Taylor"] }, + { value: "FOURTH", label: "Fourth", counties: ["Clay", "Duval", "Nassau"] }, + { value: "FIFTH", label: "Fifth", counties: ["Citrus", "Hernando", "Lake", "Marion", "Sumter"] }, + { value: "SIXTH", label: "Sixth", counties: ["Pasco", "Pinellas"] }, + { value: "SEVENTH", label: "Seventh", counties: ["Flagler", "Putnam", "St. Johns", "Volusia"] }, + { value: "EIGHTH", label: "Eighth", counties: ["Alachua", "Baker", "Bradford", "Gilchrist", "Levy", "Union"] }, + { value: "NINTH", label: "Ninth", counties: ["Orange", "Osceola"] }, + { value: "TENTH", label: "Tenth", counties: ["Hardee", "Highlands", "Polk"] }, + { value: "ELEVENTH", label: "Eleventh", counties: ["Miami-Dade"] }, + { value: "TWELFTH", label: "Twelfth", counties: ["DeSoto", "Manatee", "Sarasota"] }, + { value: "THIRTEENTH", label: "Thirteenth", counties: ["Hillsborough"] }, + { value: "FOURTEENTH", label: "Fourteenth", counties: ["Bay", "Calhoun", "Gulf", "Holmes", "Jackson", "Washington"] }, + { value: "FIFTEENTH", label: "Fifteenth", counties: ["Palm Beach"] }, + { value: "SIXTEENTH", label: "Sixteenth", counties: ["Monroe"] }, + { value: "SEVENTEENTH", label: "Seventeenth", counties: ["Broward"] }, + { value: "EIGHTEENTH", label: "Eighteenth", counties: ["Brevard", "Seminole"] }, + { value: "NINETEENTH", label: "Nineteenth", counties: ["Indian River", "Martin", "Okeechobee", "St. Lucie"] }, + { value: "TWENTIETH", label: "Twentieth", counties: ["Charlotte", "Collier", "Glades", "Hendry", "Lee"] }, +]; + +export const FL_COUNTIES: string[] = Array.from( + new Set(FL_CIRCUITS.flatMap((c) => c.counties)), +).sort();