Added save-to-case for forms/ledgers
X-Lovable-Edit-ID: edt-d57aff93-c9d1-491d-bfb1-83b80d6b465e Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com>
This commit is contained in:
@@ -11,12 +11,14 @@ import {
|
||||
fmtCurrency,
|
||||
fmtDateLong,
|
||||
ownerFullName,
|
||||
savePdfToDocuments,
|
||||
type ClientLite,
|
||||
type HomeownerLite,
|
||||
type FirmInfo,
|
||||
} from "@/lib/forms-shared";
|
||||
import { buildLetterTokens, loadFormTemplate, renderLetterPdf, type FormTemplateConfig } from "@/lib/form-templates";
|
||||
import { promptFilename } from "@/lib/prompt-filename";
|
||||
import { SaveToCaseDialog } from "./save-to-case-dialog";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export function ItfForm() {
|
||||
@@ -26,6 +28,7 @@ export function ItfForm() {
|
||||
const [homeownerId, setHomeownerId] = useState("");
|
||||
const [firm, setFirm] = useState<FirmInfo | null>(null);
|
||||
const [template, setTemplate] = useState<FormTemplateConfig | null>(null);
|
||||
const [saveToCaseOpen, setSaveToCaseOpen] = useState(false);
|
||||
|
||||
const [letterDate, setLetterDate] = useState(new Date().toISOString().slice(0, 10));
|
||||
const [certifiedNo, setCertifiedNo] = useState("");
|
||||
@@ -55,10 +58,10 @@ export function ItfForm() {
|
||||
(parseFloat(adminFees) || 0) -
|
||||
(parseFloat(lessPayments) || 0);
|
||||
|
||||
const handleExport = () => {
|
||||
const buildPdf = () => {
|
||||
if (!client || !template) {
|
||||
toast.error("Select a client first");
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
const returnAddress = [client.name];
|
||||
@@ -68,7 +71,6 @@ export function ItfForm() {
|
||||
const recipient: string[] = [ownerFullName(homeowner) || "[Homeowner]"];
|
||||
if (homeowner?.address) recipient.push(homeowner.address);
|
||||
|
||||
// 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${
|
||||
@@ -95,7 +97,7 @@ export function ItfForm() {
|
||||
extra: { lienRef },
|
||||
});
|
||||
|
||||
const doc = renderLetterPdf({
|
||||
return renderLetterPdf({
|
||||
template,
|
||||
returnAddress,
|
||||
topRightLine: tokens.date as string,
|
||||
@@ -106,13 +108,33 @@ export function ItfForm() {
|
||||
total: { label: labels.totalDue ?? "TOTAL DUE", amount: total },
|
||||
filename: `ITF_${ownerFullName(homeowner) || "draft"}_${letterDate}`,
|
||||
});
|
||||
};
|
||||
|
||||
const name = promptFilename(`ITF_${ownerFullName(homeowner) || "draft"}_${letterDate}`, "pdf");
|
||||
const defaultName = `ITF_${ownerFullName(homeowner) || "draft"}_${letterDate}`;
|
||||
|
||||
const handleExport = () => {
|
||||
const doc = buildPdf();
|
||||
if (!doc) return;
|
||||
const name = promptFilename(defaultName, "pdf");
|
||||
if (!name) return;
|
||||
doc.save(`${name}.pdf`);
|
||||
toast.success("ITF PDF downloaded");
|
||||
};
|
||||
|
||||
const handleSaveToCase = async ({ caseId, name, folder }: { caseId: string; name: string; folder: string }) => {
|
||||
const doc = buildPdf();
|
||||
if (!doc) return;
|
||||
const filename = name.endsWith(".pdf") ? name : `${name}.pdf`;
|
||||
await savePdfToDocuments({
|
||||
blob: doc.output("blob"),
|
||||
caseId,
|
||||
name: filename,
|
||||
folder,
|
||||
mimeType: "application/pdf",
|
||||
});
|
||||
toast.success("Saved to case files");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<Card>
|
||||
@@ -223,13 +245,27 @@ export function ItfForm() {
|
||||
|
||||
<div className="flex items-center justify-between pt-2">
|
||||
<span className="text-sm font-medium">Total due: ${fmtCurrency(total)}</span>
|
||||
<Button onClick={handleExport} disabled={!template}>
|
||||
<FileDown className="h-4 w-4 mr-2" />
|
||||
Export PDF
|
||||
</Button>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" onClick={() => setSaveToCaseOpen(true)} disabled={!template || !client}>
|
||||
<FileDown className="h-4 w-4 mr-2" />
|
||||
Save to case
|
||||
</Button>
|
||||
<Button onClick={handleExport} disabled={!template}>
|
||||
<FileDown className="h-4 w-4 mr-2" />
|
||||
Export PDF
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<SaveToCaseDialog
|
||||
open={saveToCaseOpen}
|
||||
onOpenChange={setSaveToCaseOpen}
|
||||
defaultName={defaultName}
|
||||
defaultFolder="Forms & Letters"
|
||||
onConfirm={handleSaveToCase}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -11,12 +11,14 @@ import {
|
||||
fmtCurrency,
|
||||
fmtDateLong,
|
||||
ownerFullName,
|
||||
savePdfToDocuments,
|
||||
type ClientLite,
|
||||
type HomeownerLite,
|
||||
type FirmInfo,
|
||||
} from "@/lib/forms-shared";
|
||||
import { buildLetterTokens, loadFormTemplate, renderLetterPdf, type FormTemplateConfig } from "@/lib/form-templates";
|
||||
import { promptFilename } from "@/lib/prompt-filename";
|
||||
import { SaveToCaseDialog } from "./save-to-case-dialog";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export function ItlForm() {
|
||||
@@ -26,6 +28,7 @@ export function ItlForm() {
|
||||
const [homeownerId, setHomeownerId] = useState("");
|
||||
const [firm, setFirm] = useState<FirmInfo | null>(null);
|
||||
const [template, setTemplate] = useState<FormTemplateConfig | null>(null);
|
||||
const [saveToCaseOpen, setSaveToCaseOpen] = useState(false);
|
||||
|
||||
const [letterDate, setLetterDate] = useState(new Date().toISOString().slice(0, 10));
|
||||
const [certifiedNo, setCertifiedNo] = useState("");
|
||||
@@ -53,10 +56,10 @@ export function ItlForm() {
|
||||
(parseFloat(adminFees) || 0) -
|
||||
(parseFloat(lessPayments) || 0);
|
||||
|
||||
const handleExport = () => {
|
||||
const buildPdf = () => {
|
||||
if (!client || !template) {
|
||||
toast.error("Select a client first");
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
const returnAddress = [client.name];
|
||||
@@ -84,7 +87,7 @@ export function ItlForm() {
|
||||
total,
|
||||
});
|
||||
|
||||
const doc = renderLetterPdf({
|
||||
return renderLetterPdf({
|
||||
template,
|
||||
returnAddress,
|
||||
topRightLine: tokens.date as string,
|
||||
@@ -95,13 +98,33 @@ export function ItlForm() {
|
||||
total: { label: labels.totalDue ?? "TOTAL DUE", amount: total },
|
||||
filename: `ITL_${ownerFullName(homeowner) || "draft"}_${letterDate}`,
|
||||
});
|
||||
};
|
||||
|
||||
const name = promptFilename(`ITL_${ownerFullName(homeowner) || "draft"}_${letterDate}`, "pdf");
|
||||
const defaultName = `ITL_${ownerFullName(homeowner) || "draft"}_${letterDate}`;
|
||||
|
||||
const handleExport = () => {
|
||||
const doc = buildPdf();
|
||||
if (!doc) return;
|
||||
const name = promptFilename(defaultName, "pdf");
|
||||
if (!name) return;
|
||||
doc.save(`${name}.pdf`);
|
||||
toast.success("ITL PDF downloaded");
|
||||
};
|
||||
|
||||
const handleSaveToCase = async ({ caseId, name, folder }: { caseId: string; name: string; folder: string }) => {
|
||||
const doc = buildPdf();
|
||||
if (!doc) return;
|
||||
const filename = name.endsWith(".pdf") ? name : `${name}.pdf`;
|
||||
await savePdfToDocuments({
|
||||
blob: doc.output("blob"),
|
||||
caseId,
|
||||
name: filename,
|
||||
folder,
|
||||
mimeType: "application/pdf",
|
||||
});
|
||||
toast.success("Saved to case files");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<Card>
|
||||
@@ -192,13 +215,27 @@ export function ItlForm() {
|
||||
|
||||
<div className="flex items-center justify-between pt-2">
|
||||
<span className="text-sm font-medium">Total due: ${fmtCurrency(total)}</span>
|
||||
<Button onClick={handleExport} disabled={!template}>
|
||||
<FileDown className="h-4 w-4 mr-2" />
|
||||
Export PDF
|
||||
</Button>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" onClick={() => setSaveToCaseOpen(true)} disabled={!template || !client}>
|
||||
<FileDown className="h-4 w-4 mr-2" />
|
||||
Save to case
|
||||
</Button>
|
||||
<Button onClick={handleExport} disabled={!template}>
|
||||
<FileDown className="h-4 w-4 mr-2" />
|
||||
Export PDF
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<SaveToCaseDialog
|
||||
open={saveToCaseOpen}
|
||||
onOpenChange={setSaveToCaseOpen}
|
||||
defaultName={defaultName}
|
||||
defaultFolder="Forms & Letters"
|
||||
onConfirm={handleSaveToCase}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,12 +10,14 @@ import {
|
||||
fetchFirm,
|
||||
fmtCurrency,
|
||||
ownerFullName,
|
||||
savePdfToDocuments,
|
||||
type ClientLite,
|
||||
type HomeownerLite,
|
||||
type FirmInfo,
|
||||
} from "@/lib/forms-shared";
|
||||
import { buildLetterTokens, loadFormTemplate, renderLetterPdf, type FormTemplateConfig } from "@/lib/form-templates";
|
||||
import { promptFilename } from "@/lib/prompt-filename";
|
||||
import { SaveToCaseDialog } from "./save-to-case-dialog";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface Item {
|
||||
@@ -30,6 +32,7 @@ export function NolaForm() {
|
||||
const [homeownerId, setHomeownerId] = useState("");
|
||||
const [firm, setFirm] = useState<FirmInfo | null>(null);
|
||||
const [template, setTemplate] = useState<FormTemplateConfig | null>(null);
|
||||
const [saveToCaseOpen, setSaveToCaseOpen] = useState(false);
|
||||
|
||||
const [date, setDate] = useState(new Date().toISOString().slice(0, 10));
|
||||
const [dueDate, setDueDate] = useState(() => {
|
||||
@@ -61,10 +64,10 @@ export function NolaForm() {
|
||||
const addItem = () => setItems((p) => [...p, { description: "", amount: "0.00" }]);
|
||||
const removeItem = (i: number) => setItems((p) => p.filter((_, idx) => idx !== i));
|
||||
|
||||
const handleExport = () => {
|
||||
const buildPdf = () => {
|
||||
if (!client || !template) {
|
||||
toast.error("Select a client first");
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
const returnAddress = [client.name];
|
||||
@@ -83,7 +86,7 @@ export function NolaForm() {
|
||||
total,
|
||||
});
|
||||
|
||||
const doc = renderLetterPdf({
|
||||
return renderLetterPdf({
|
||||
template,
|
||||
returnAddress,
|
||||
topRightLine: tokens.date as string,
|
||||
@@ -94,13 +97,33 @@ export function NolaForm() {
|
||||
total: { label: template.itemLabels?.totalDue ?? "TOTAL DUE", amount: total },
|
||||
filename: `NOLA_${ownerFullName(homeowner) || "draft"}_${date}`,
|
||||
});
|
||||
};
|
||||
|
||||
const name = promptFilename(`NOLA_${ownerFullName(homeowner) || "draft"}_${date}`, "pdf");
|
||||
const defaultName = `NOLA_${ownerFullName(homeowner) || "draft"}_${date}`;
|
||||
|
||||
const handleExport = () => {
|
||||
const doc = buildPdf();
|
||||
if (!doc) return;
|
||||
const name = promptFilename(defaultName, "pdf");
|
||||
if (!name) return;
|
||||
doc.save(`${name}.pdf`);
|
||||
toast.success("NOLA PDF downloaded");
|
||||
};
|
||||
|
||||
const handleSaveToCase = async ({ caseId, name, folder }: { caseId: string; name: string; folder: string }) => {
|
||||
const doc = buildPdf();
|
||||
if (!doc) return;
|
||||
const filename = name.endsWith(".pdf") ? name : `${name}.pdf`;
|
||||
await savePdfToDocuments({
|
||||
blob: doc.output("blob"),
|
||||
caseId,
|
||||
name: filename,
|
||||
folder,
|
||||
mimeType: "application/pdf",
|
||||
});
|
||||
toast.success("Saved to case files");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<Card>
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||
import { fetchAccessibleCases, type CaseLite } from "@/lib/forms-shared";
|
||||
import { FileText, Loader2 } from "lucide-react";
|
||||
|
||||
export type SaveFormat = "pdf" | "docx" | "csv";
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
onOpenChange: (v: boolean) => void;
|
||||
/** Pre-selected case (e.g. when called from a case detail screen). */
|
||||
defaultCaseId?: string;
|
||||
/** Allowed save formats. Defaults to ["pdf"]. */
|
||||
formats?: SaveFormat[];
|
||||
/** Suggested base filename (no extension). */
|
||||
defaultName: string;
|
||||
/** Folder name within case-documents. Defaults to "Forms & Letters". */
|
||||
defaultFolder?: string;
|
||||
/** Called when the user confirms. Should perform the actual upload. */
|
||||
onConfirm: (opts: {
|
||||
caseId: string;
|
||||
name: string;
|
||||
format: SaveFormat;
|
||||
folder: string;
|
||||
}) => Promise<void> | void;
|
||||
title?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reusable picker for saving a generated document directly into a case's
|
||||
* Documents tab. Loads accessible cases on first open.
|
||||
*/
|
||||
export function SaveToCaseDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
defaultCaseId,
|
||||
formats = ["pdf"],
|
||||
defaultName,
|
||||
defaultFolder = "Forms & Letters",
|
||||
onConfirm,
|
||||
title = "Save to case files",
|
||||
description = "Pick a case and the file will be uploaded to its Documents tab.",
|
||||
}: Props) {
|
||||
const [cases, setCases] = useState<CaseLite[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [caseId, setCaseId] = useState(defaultCaseId ?? "");
|
||||
const [name, setName] = useState(defaultName);
|
||||
const [folder, setFolder] = useState(defaultFolder);
|
||||
const [format, setFormat] = useState<SaveFormat>(formats[0]);
|
||||
const [search, setSearch] = useState("");
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
// Load cases the first time the dialog opens.
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
setName(defaultName);
|
||||
setCaseId(defaultCaseId ?? "");
|
||||
if (cases.length === 0) {
|
||||
setLoading(true);
|
||||
fetchAccessibleCases()
|
||||
.then(setCases)
|
||||
.finally(() => setLoading(false));
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [open]);
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
const q = search.trim().toLowerCase();
|
||||
if (!q) return cases.slice(0, 50);
|
||||
return cases
|
||||
.filter(
|
||||
(c) =>
|
||||
c.case_number.toLowerCase().includes(q) ||
|
||||
c.title.toLowerCase().includes(q),
|
||||
)
|
||||
.slice(0, 50);
|
||||
}, [cases, search]);
|
||||
|
||||
const handleConfirm = async () => {
|
||||
if (!caseId || !name.trim()) return;
|
||||
setSaving(true);
|
||||
try {
|
||||
await onConfirm({ caseId, name: name.trim(), format, folder: folder.trim() || "Forms & Letters" });
|
||||
onOpenChange(false);
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
<DialogDescription>{description}</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label>Search cases</Label>
|
||||
<Input
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
placeholder="Case number or title…"
|
||||
/>
|
||||
<div className="mt-2 max-h-56 overflow-auto rounded-md border">
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center p-4 text-sm text-muted-foreground">
|
||||
<Loader2 className="h-4 w-4 mr-2 animate-spin" /> Loading cases…
|
||||
</div>
|
||||
) : filtered.length === 0 ? (
|
||||
<div className="p-3 text-sm text-muted-foreground">No cases match.</div>
|
||||
) : (
|
||||
filtered.map((c) => (
|
||||
<button
|
||||
key={c.id}
|
||||
type="button"
|
||||
onClick={() => setCaseId(c.id)}
|
||||
className={`block w-full text-left px-3 py-2 border-b last:border-b-0 text-sm hover:bg-muted ${
|
||||
caseId === c.id ? "bg-muted font-medium" : ""
|
||||
}`}
|
||||
>
|
||||
<div>{c.title}</div>
|
||||
<div className="text-xs text-muted-foreground">{c.case_number}</div>
|
||||
</button>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
<div>
|
||||
<Label>File name</Label>
|
||||
<Input value={name} onChange={(e) => setName(e.target.value)} />
|
||||
</div>
|
||||
<div>
|
||||
<Label>Folder</Label>
|
||||
<Input value={folder} onChange={(e) => setFolder(e.target.value)} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{formats.length > 1 && (
|
||||
<div>
|
||||
<Label>Format</Label>
|
||||
<RadioGroup
|
||||
value={format}
|
||||
onValueChange={(v) => setFormat(v as SaveFormat)}
|
||||
className="flex gap-4 mt-2"
|
||||
>
|
||||
{formats.map((f) => (
|
||||
<div key={f} className="flex items-center gap-2">
|
||||
<RadioGroupItem value={f} id={`fmt-${f}`} />
|
||||
<Label htmlFor={`fmt-${f}`} className="text-sm uppercase">
|
||||
{f}
|
||||
</Label>
|
||||
</div>
|
||||
))}
|
||||
</RadioGroup>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="ghost" onClick={() => onOpenChange(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button disabled={!caseId || !name.trim() || saving} onClick={handleConfirm}>
|
||||
{saving ? (
|
||||
<Loader2 className="h-4 w-4 mr-1.5 animate-spin" />
|
||||
) : (
|
||||
<FileText className="h-4 w-4 mr-1.5" />
|
||||
)}
|
||||
Save to case
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
+11
-1
@@ -196,7 +196,7 @@ function drawFooter(pdf: jsPDF, page: number, totalPages: number, left?: string,
|
||||
pdf.text(rightText, PAGE_W - MARGIN - rw, y);
|
||||
}
|
||||
|
||||
export async function downloadPleadingPdf(input: PleadingInput, filename: string) {
|
||||
async function buildPleadingPdf(input: PleadingInput): Promise<jsPDF> {
|
||||
const pdf = new jsPDF({ unit: "pt", format: "letter" });
|
||||
registerBookmanFont(pdf);
|
||||
registerBirdsFont(pdf);
|
||||
@@ -415,5 +415,15 @@ export async function downloadPleadingPdf(input: PleadingInput, filename: string
|
||||
drawFooter(pdf, i, total, input.footerLeft, input.footerCenter, input.footerRight);
|
||||
}
|
||||
|
||||
return pdf;
|
||||
}
|
||||
|
||||
export async function downloadPleadingPdf(input: PleadingInput, filename: string) {
|
||||
const pdf = await buildPleadingPdf(input);
|
||||
pdf.save(filename.endsWith(".pdf") ? filename : `${filename}.pdf`);
|
||||
}
|
||||
|
||||
export async function buildPleadingPdfBlob(input: PleadingInput): Promise<Blob> {
|
||||
const pdf = await buildPleadingPdf(input);
|
||||
return pdf.output("blob");
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ import { Switch } from "@/components/ui/switch";
|
||||
import { FL_CIRCUITS } from "@/lib/florida";
|
||||
import { Packer } from "docx";
|
||||
import { buildPleadingDoc, downloadPleading, type PleadingFootnote, type PleadingSignature, type ServiceContact } from "@/lib/docx-pleading";
|
||||
import { downloadPleadingPdf } from "@/lib/pdf-pleading";
|
||||
import { downloadPleadingPdf, buildPleadingPdfBlob } 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";
|
||||
@@ -25,6 +25,8 @@ import { Download, FileText, Save, Loader2, Plus, Trash2, PenLine, Users, Variab
|
||||
import { applyVars, buildChips, buildVarMap, loadPleadingContext, type LoadedContext, type VarChip } from "@/lib/pleading-variables";
|
||||
import { promptFilename } from "@/lib/prompt-filename";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { SaveToCaseDialog } from "@/components/forms/save-to-case-dialog";
|
||||
import { savePdfToDocuments } from "@/lib/forms-shared";
|
||||
|
||||
export const Route = createFileRoute("/documents/pleading/new")({
|
||||
component: PleadingNewPage,
|
||||
@@ -53,6 +55,7 @@ function PleadingNewPage() {
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [serviceList, setServiceList] = useState<ServiceContact[]>([]);
|
||||
const [serviceListTitle, setServiceListTitle] = useState("SERVICE LIST");
|
||||
const [saveToCaseOpen, setSaveToCaseOpen] = useState(false);
|
||||
|
||||
// Client/case linkage for variable substitution
|
||||
const [clientId, setClientId] = useState<string>("");
|
||||
@@ -257,6 +260,41 @@ function PleadingNewPage() {
|
||||
await downloadPleadingPdf(input, name);
|
||||
};
|
||||
|
||||
const onSaveToCase = async ({
|
||||
caseId: targetCaseId,
|
||||
name,
|
||||
format,
|
||||
folder,
|
||||
}: { caseId: string; name: string; format: "pdf" | "docx" | "csv"; folder: string }) => {
|
||||
try {
|
||||
let blob: Blob;
|
||||
let mimeType: string;
|
||||
let ext: string;
|
||||
if (format === "docx") {
|
||||
const doc = buildPleadingDoc(input);
|
||||
blob = await Packer.toBlob(doc);
|
||||
mimeType = "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
|
||||
ext = "docx";
|
||||
} else {
|
||||
blob = await buildPleadingPdfBlob(input);
|
||||
mimeType = "application/pdf";
|
||||
ext = "pdf";
|
||||
}
|
||||
const filename = name.endsWith(`.${ext}`) ? name : `${name}.${ext}`;
|
||||
await savePdfToDocuments({
|
||||
blob,
|
||||
caseId: targetCaseId,
|
||||
name: filename,
|
||||
folder,
|
||||
mimeType,
|
||||
});
|
||||
toast.success("Saved to case files");
|
||||
} catch (e: any) {
|
||||
toast.error(e.message ?? "Save failed");
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
const onSave = async () => {
|
||||
setSaving(true);
|
||||
try {
|
||||
@@ -306,6 +344,9 @@ function PleadingNewPage() {
|
||||
<>
|
||||
<Button variant="outline" onClick={onDownloadPdf}><FileText className="h-4 w-4 mr-2" /> Download PDF</Button>
|
||||
<Button variant="outline" onClick={onDownload}><Download className="h-4 w-4 mr-2" /> Download .docx</Button>
|
||||
<Button variant="outline" onClick={() => setSaveToCaseOpen(true)}>
|
||||
<FileText className="h-4 w-4 mr-2" /> Save to case
|
||||
</Button>
|
||||
<Button onClick={onSave} disabled={saving}>
|
||||
{saving ? <Loader2 className="h-4 w-4 mr-2 animate-spin" /> : <Save className="h-4 w-4 mr-2" />}
|
||||
Save & download
|
||||
@@ -799,6 +840,16 @@ function PleadingNewPage() {
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<SaveToCaseDialog
|
||||
open={saveToCaseOpen}
|
||||
onOpenChange={setSaveToCaseOpen}
|
||||
defaultCaseId={caseId || undefined}
|
||||
formats={["pdf", "docx"]}
|
||||
defaultName={(docName || "Pleading").replace(/[^a-zA-Z0-9._-]/g, "_")}
|
||||
defaultFolder="Pleadings"
|
||||
onConfirm={onSaveToCase}
|
||||
/>
|
||||
</PageContainer>
|
||||
</ProtectedLayout>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user