"use client"; import { AlertTriangle, Check, 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 { 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 }: { items: ActionPreviewData[] }) { const { t } = useTranslation(); const [open, setOpen] = useState(false); const [removed, setRemoved] = useState>(new Set()); const [status, setStatus] = useState("pending"); const [result, setResult] = useState<{ added: number; failed: number } | null>( null, ); 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); added += 1; } catch { failed += 1; } } setResult({ added, failed }); setStatus("done"); setOpen(false); 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); }; 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 })}` : ""}

) : 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 lines = summarize(it); const record = it.record as Record; const newPatient = it.kind === "appointment" && !record.fileNumber; return (
{lines.map((line, i) => ( {line} ))} {newPatient ? ( {t("chat.actionCard.batch.newPatient")} ) : null} {it.issues && it.issues.length > 0 ? ( {it.issues[0]} ) : null}
); }) )}
); }