"use client"; import { AlertTriangle, Check, Pencil, Sparkles, X } from "lucide-react"; import { useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { ACTION_ICONS, commitAction, summarize, } from "@/components/chat/action-preview-card"; import { RecordEditDialog } from "@/components/chat/record-edit-dialog"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card } from "@/components/ui/card"; import { Dialog, DialogDescription, DialogFooter, DialogHeader, DialogPanel, DialogPopup, DialogTitle, } from "@/components/ui/dialog"; import type { ActionPreviewData } from "@/lib/ai-chat"; import { notify } from "@/lib/toast"; type Status = "pending" | "committing" | "done" | "rejected"; // A single approval surface for many agent-proposed records (e.g. an imported // file of appointments) instead of one card per record. The clinician reviews // the full list in a dialog, removes any they don't want, and adds them all at // once. Each commit goes through the same RBAC-gated create endpoint as the // single-record card; appointments without a file number create a patient. export function BatchActionPreviewCard({ items, onResolved, }: { items: ActionPreviewData[]; // Called once committed/discarded so the parent can persist the resolution // across re-render and conversation reload (prevents re-adding). onResolved?: (resolution: "added" | "discarded") => void; }) { const { t } = useTranslation(); const [open, setOpen] = useState(false); const [removed, setRemoved] = useState>(new Set()); const [status, setStatus] = useState( items[0]?.resolved === "added" ? "done" : items[0]?.resolved === "discarded" ? "rejected" : "pending", ); const [result, setResult] = useState<{ added: number; failed: number } | null>( null, ); // Per-row edits, keyed by token. The committed record is the edit if present, // otherwise the agent's original proposal. const [edits, setEdits] = useState>>( {}, ); // The row currently open in the edit dialog. const [editing, setEditing] = useState(null); const recordFor = (it: ActionPreviewData) => edits[it.token] ?? (it.record as Record); const kept = useMemo( () => items.filter((it) => !removed.has(it.token)), [items, removed], ); const Icon = ACTION_ICONS[items[0]?.kind ?? "appointment"]; const addAll = async () => { setStatus("committing"); let added = 0; let failed = 0; // Sequential so server-side patient de-dup (by name) sees prior creates. for (const it of kept) { try { await commitAction({ ...it, record: recordFor(it) }); added += 1; } catch { failed += 1; } } setResult({ added, failed }); setStatus("done"); setOpen(false); onResolved?.("added"); if (added > 0) { notify.success( t("chat.actionCard.addedTitle"), t("chat.actionCard.batch.done", { added, total: kept.length }), ); } else if (failed > 0) { notify.error( t("chat.actionCard.failedTitle"), t("chat.actionCard.failedBody"), ); } }; const discardAll = () => { setStatus("rejected"); setOpen(false); onResolved?.("discarded"); }; return (
{t("chat.actionCard.batch.title", { count: items.length })} AI
{status === "done" ? (

{result ? `${t("chat.actionCard.batch.done", { added: result.added, total: items.length, })}${ result.failed > 0 ? ` ยท ${t("chat.actionCard.batch.failedCount", { count: result.failed })}` : "" }` : t("chat.actionCard.batch.alreadyAdded")}

) : status === "rejected" ? (

{t("chat.actionCard.batch.discarded")}

) : (
)} {t("chat.actionCard.batch.dialogTitle")} {t("chat.actionCard.batch.dialogDescription")} {kept.length === 0 ? (

{t("chat.actionCard.batch.discarded")}

) : ( kept.map((it) => { const record = recordFor(it); const lines = summarize({ ...it, record }); const newPatient = it.kind === "appointment" && !record.fileNumber; const edited = it.token in edits; return (
{lines.map((line, i) => ( {line} ))} {edited ? ( {t("chat.actionCard.batch.edited")} ) : null} {newPatient ? ( {t("chat.actionCard.batch.newPatient")} ) : null} {it.issues && it.issues.length > 0 ? ( {it.issues[0]} ) : null}
); }) )}
{editing ? ( { if (!o) setEditing(null); }} onSave={(record) => setEdits((prev) => ({ ...prev, [editing.token]: record })) } open={editing !== null} record={editing.record as Record} /> ) : null}
); }