Changes
Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com>
This commit is contained in:
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user