Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com>
This commit is contained in:
gpt-engineer-app[bot]
2026-04-20 00:38:26 +00:00
co-authored by renee-png
parent 033501e2c8
commit 78c7e16db0
@@ -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>
);
}