diff --git a/src/components/cases/collections-tab.tsx b/src/components/cases/collections-tab.tsx index bb93ed6..0cf5247 100644 --- a/src/components/cases/collections-tab.tsx +++ b/src/components/cases/collections-tab.tsx @@ -31,6 +31,7 @@ import { import { supabase } from "@/integrations/supabase/client"; import { useAuth } from "@/lib/auth"; import { formatCurrency, formatDate } from "@/lib/format"; +import { promptFilename } from "@/lib/prompt-filename"; import { ArrowLeft, Calculator, @@ -544,9 +545,11 @@ export function CollectionDetail({ ].join(",")), ].join("\n"); const blob = new Blob([rows], { type: "text/csv" }); + const name = promptFilename(`Statement_${ho?.last_name || "homeowner"}`, "csv"); + if (!name) return; const a = document.createElement("a"); a.href = URL.createObjectURL(blob); - a.download = `Statement_${ho?.last_name || "homeowner"}.csv`; + a.download = `${name}.csv`; a.click(); }; diff --git a/src/components/forms/custom-form-builder.tsx b/src/components/forms/custom-form-builder.tsx index 54e7e68..e53229a 100644 --- a/src/components/forms/custom-form-builder.tsx +++ b/src/components/forms/custom-form-builder.tsx @@ -91,6 +91,7 @@ import { } from "docx"; import { toast } from "sonner"; import { supabase } from "@/integrations/supabase/client"; +import { promptFilename } from "@/lib/prompt-filename"; // Paragraph extension with indent (px) + caption flag, persisted as inline styles const PaddedParagraph = Paragraph.extend({ @@ -1026,19 +1027,21 @@ export function CustomFormBuilder() { y += seg.caption ? 2 : 4; } - const filenameBase = (renderTitle(ctx) || "Custom_Form").replace(/\s+/g, "_"); + const defaultName = (renderTitle(ctx) || "Custom_Form").replace(/\s+/g, "_"); if (saveToCase && selectedCaseId) { const blob = doc.output("blob"); await savePdfToDocuments({ blob, caseId: selectedCaseId, - name: `${filenameBase}.pdf`, + name: `${defaultName}.pdf`, mimeType: "application/pdf", }); toast.success("Saved to case files"); setSaveToCaseOpen(false); } else { - doc.save(`${filenameBase}.pdf`); + const name = promptFilename(defaultName, "pdf"); + if (!name) return; + doc.save(`${name}.pdf`); toast.success("PDF downloaded"); } }; @@ -1236,22 +1239,24 @@ export function CustomFormBuilder() { }); const blob = await Packer.toBlob(docx); - const filenameBase = (renderTitle(ctx) || "Custom_Form").replace(/\s+/g, "_"); + const defaultName = (renderTitle(ctx) || "Custom_Form").replace(/\s+/g, "_"); if (saveToCase && selectedCaseId) { await savePdfToDocuments({ blob, caseId: selectedCaseId, - name: `${filenameBase}.docx`, + name: `${defaultName}.docx`, mimeType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document", }); toast.success("Saved to case files"); setSaveToCaseOpen(false); } else { + const name = promptFilename(defaultName, "docx"); + if (!name) return; const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; - a.download = `${filenameBase}.docx`; + a.download = `${name}.docx`; a.click(); URL.revokeObjectURL(url); toast.success("DOCX downloaded"); diff --git a/src/components/forms/estoppel-form.tsx b/src/components/forms/estoppel-form.tsx index a2c7d62..51c5b16 100644 --- a/src/components/forms/estoppel-form.tsx +++ b/src/components/forms/estoppel-form.tsx @@ -15,6 +15,7 @@ import { type FirmInfo, } from "@/lib/forms-shared"; import { fillTokens, loadFormTemplate, type FormTemplateConfig } from "@/lib/form-templates"; +import { promptFilename } from "@/lib/prompt-filename"; import { jsPDF } from "jspdf"; import { toast } from "sonner"; @@ -266,7 +267,9 @@ export function EstoppelForm() { doc.text(al, margin, y); } - doc.save(`Estoppel_${parcelId || "draft"}_${date}.pdf`); + const name = promptFilename(`Estoppel_${parcelId || "draft"}_${date}`, "pdf"); + if (!name) return; + doc.save(`${name}.pdf`); toast.success("Estoppel PDF downloaded"); }; diff --git a/src/components/forms/itf-form.tsx b/src/components/forms/itf-form.tsx index 47e2a24..21f6c22 100644 --- a/src/components/forms/itf-form.tsx +++ b/src/components/forms/itf-form.tsx @@ -16,6 +16,7 @@ import { type FirmInfo, } from "@/lib/forms-shared"; import { buildLetterTokens, loadFormTemplate, renderLetterPdf, type FormTemplateConfig } from "@/lib/form-templates"; +import { promptFilename } from "@/lib/prompt-filename"; import { toast } from "sonner"; export function ItfForm() { @@ -106,7 +107,9 @@ export function ItfForm() { filename: `ITF_${ownerFullName(homeowner) || "draft"}_${letterDate}`, }); - doc.save(`ITF_${ownerFullName(homeowner) || "draft"}_${letterDate}.pdf`); + const name = promptFilename(`ITF_${ownerFullName(homeowner) || "draft"}_${letterDate}`, "pdf"); + if (!name) return; + doc.save(`${name}.pdf`); toast.success("ITF PDF downloaded"); }; diff --git a/src/components/forms/itl-form.tsx b/src/components/forms/itl-form.tsx index 74dedd9..15722fc 100644 --- a/src/components/forms/itl-form.tsx +++ b/src/components/forms/itl-form.tsx @@ -16,6 +16,7 @@ import { type FirmInfo, } from "@/lib/forms-shared"; import { buildLetterTokens, loadFormTemplate, renderLetterPdf, type FormTemplateConfig } from "@/lib/form-templates"; +import { promptFilename } from "@/lib/prompt-filename"; import { toast } from "sonner"; export function ItlForm() { @@ -95,7 +96,9 @@ export function ItlForm() { filename: `ITL_${ownerFullName(homeowner) || "draft"}_${letterDate}`, }); - doc.save(`ITL_${ownerFullName(homeowner) || "draft"}_${letterDate}.pdf`); + const name = promptFilename(`ITL_${ownerFullName(homeowner) || "draft"}_${letterDate}`, "pdf"); + if (!name) return; + doc.save(`${name}.pdf`); toast.success("ITL PDF downloaded"); }; diff --git a/src/components/forms/letter-generator.tsx b/src/components/forms/letter-generator.tsx index 8342947..c16e289 100644 --- a/src/components/forms/letter-generator.tsx +++ b/src/components/forms/letter-generator.tsx @@ -17,6 +17,7 @@ import { type FirmInfo, } from "@/lib/forms-shared"; import { fillTokens, loadFormTemplate, type FormTemplateConfig } from "@/lib/form-templates"; +import { promptFilename } from "@/lib/prompt-filename"; import { supabase } from "@/integrations/supabase/client"; import { jsPDF } from "jspdf"; import { toast } from "sonner"; @@ -290,7 +291,9 @@ export function LetterGenerator() { } } - doc.save(`Letter_${date}.pdf`); + const name = promptFilename(`Letter_${date}`, "pdf"); + if (!name) return; + doc.save(`${name}.pdf`); toast.success("Letter PDF downloaded"); }; diff --git a/src/components/forms/nola-form.tsx b/src/components/forms/nola-form.tsx index 7f1ca25..a5d4f29 100644 --- a/src/components/forms/nola-form.tsx +++ b/src/components/forms/nola-form.tsx @@ -15,6 +15,7 @@ import { type FirmInfo, } from "@/lib/forms-shared"; import { buildLetterTokens, loadFormTemplate, renderLetterPdf, type FormTemplateConfig } from "@/lib/form-templates"; +import { promptFilename } from "@/lib/prompt-filename"; import { toast } from "sonner"; interface Item { @@ -94,7 +95,9 @@ export function NolaForm() { filename: `NOLA_${ownerFullName(homeowner) || "draft"}_${date}`, }); - doc.save(`NOLA_${ownerFullName(homeowner) || "draft"}_${date}.pdf`); + const name = promptFilename(`NOLA_${ownerFullName(homeowner) || "draft"}_${date}`, "pdf"); + if (!name) return; + doc.save(`${name}.pdf`); toast.success("NOLA PDF downloaded"); }; diff --git a/src/lib/prompt-filename.ts b/src/lib/prompt-filename.ts new file mode 100644 index 0000000..5dcaee2 --- /dev/null +++ b/src/lib/prompt-filename.ts @@ -0,0 +1,17 @@ +/** + * Prompts the user for a filename before download. Returns the chosen base + * name (without extension) sanitized for filesystem use, or null if cancelled. + * + * @param defaultName Suggested base name (no extension) + * @param ext Extension shown to the user, e.g. "pdf", "docx", "csv" + */ +export function promptFilename(defaultName: string, ext: string): string | null { + const sanitize = (s: string) => s.replace(/[\\/:*?"<>|]+/g, "_").trim(); + const cleanDefault = sanitize(defaultName).replace(new RegExp(`\\.${ext}$`, "i"), "") || "Document"; + const input = typeof window !== "undefined" + ? window.prompt(`File name (.${ext})`, cleanDefault) + : cleanDefault; + if (input === null) return null; // user cancelled + const cleaned = sanitize(input).replace(new RegExp(`\\.${ext}$`, "i"), ""); + return cleaned || cleanDefault; +} diff --git a/src/routes/documents.pleading.new.tsx b/src/routes/documents.pleading.new.tsx index 42998ac..897c953 100644 --- a/src/routes/documents.pleading.new.tsx +++ b/src/routes/documents.pleading.new.tsx @@ -22,6 +22,7 @@ import { useAuth } from "@/lib/auth"; import { toast } from "sonner"; import { Download, FileText, Save, Loader2, Plus, Trash2, PenLine, Users, Variable, Copy } from "lucide-react"; 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"; export const Route = createFileRoute("/documents/pleading/new")({ @@ -244,11 +245,15 @@ function PleadingNewPage() { const headerLine2 = `IN AND FOR ${county.toUpperCase()} COUNTY, FLORIDA`; const onDownload = async () => { - await downloadPleading(input, docName || "Pleading"); + const name = promptFilename(docName || "Pleading", "docx"); + if (!name) return; + await downloadPleading(input, name); }; const onDownloadPdf = async () => { - await downloadPleadingPdf(input, docName || "Pleading"); + const name = promptFilename(docName || "Pleading", "pdf"); + if (!name) return; + await downloadPleadingPdf(input, name); }; const onSave = async () => { diff --git a/src/routes/documents.templates.$templateId.tsx b/src/routes/documents.templates.$templateId.tsx index 0e324bd..e48b60c 100644 --- a/src/routes/documents.templates.$templateId.tsx +++ b/src/routes/documents.templates.$templateId.tsx @@ -12,6 +12,7 @@ import { useAuth } from "@/lib/auth"; import { toast } from "sonner"; import { Download, Loader2, Save, Trash2 } from "lucide-react"; import { downloadGeneric } from "@/lib/docx-pleading"; +import { promptFilename } from "@/lib/prompt-filename"; import { Packer } from "docx"; export const Route = createFileRoute("/documents/templates/$templateId")({ @@ -120,9 +121,11 @@ function TemplateDetailPage() { const fontSizePt: number = tpl?.font_size_pt || 12; const downloadOnly = async () => { + const name = promptFilename(docName || "Document", "docx"); + if (!name) return; await downloadGeneric( { title: isLetter ? undefined : docName, body: merged, fontFamily, fontSizePt }, - docName || "Document", + name, ); };