"use client"; import { AlertTriangle, Boxes, CalendarPlus, Check, ClipboardList, Pencil, Pill, Receipt, X, } from "lucide-react"; import { useState } from "react"; import { useTranslation } from "react-i18next"; import { RecordEditDialog } from "@/components/chat/record-edit-dialog"; import { Button } from "@/components/ui/button"; import { Card } from "@/components/ui/card"; import type { ActionPreviewData, ActionPreviewKind } from "@/lib/ai-chat"; import { type AppointmentInput, createAppointment } from "@/lib/appointments"; import { createInvoice, formatMoney, type InvoiceInput, type InvoiceLineItem, } from "@/lib/invoices"; import { type InventoryInput, createInventory } from "@/lib/inventory"; import { type PrescriptionInput, createPrescription } from "@/lib/prescriptions"; import { type TaskInput, createTask } from "@/lib/tasks"; import { notify } from "@/lib/toast"; type Status = "pending" | "committing" | "done" | "rejected"; export const ACTION_ICONS = { appointment: CalendarPlus, task: ClipboardList, prescription: Pill, invoice: Receipt, inventory: Boxes, } as const; const ICONS = ACTION_ICONS; // Summarise the proposed record into a couple of readable lines per kind. export function summarize(data: ActionPreviewData): string[] { const r = data.record as Record; if (data.kind === "appointment") { return [ String(r.name ?? ""), [r.date, r.time].filter(Boolean).join(" · "), [r.type, r.provider].filter(Boolean).join(" · "), ].filter(Boolean); } if (data.kind === "task") { return [ String(r.title ?? ""), [r.assignee, r.due, r.priority].filter(Boolean).join(" · "), ].filter(Boolean); } if (data.kind === "invoice") { const items = (r.lineItems as InvoiceLineItem[] | undefined) ?? []; const total = items.reduce((s, li) => s + li.quantity * li.unitPrice, 0); return [ String(r.name ?? ""), `${items.length} item${items.length === 1 ? "" : "s"} · ${formatMoney(total)}`, ].filter(Boolean); } if (data.kind === "inventory") { const items = (r.items as InventoryInput[] | undefined) ?? []; return [ `${items.length} item${items.length === 1 ? "" : "s"}`, items .map((it) => [it.name, it.strength].filter(Boolean).join(" ") + (it.stockQuantity ? ` ×${it.stockQuantity}` : ""), ) .join(", "), ].filter(Boolean); } // prescription return [ [r.medication, r.dose].filter(Boolean).join(" "), [r.frequency, r.duration].filter(Boolean).join(" · "), String(r.name ?? ""), ].filter(Boolean); } export async function commitAction(data: ActionPreviewData): Promise { // Stamp provenance so the committed record is flagged "Added by AI" and shows // up for review/editing on the relevant page. if (data.kind === "appointment") { await createAppointment({ ...(data.record as AppointmentInput), source: "ai", }); } else if (data.kind === "task") { await createTask(data.record as TaskInput); } else if (data.kind === "invoice") { await createInvoice({ ...(data.record as InvoiceInput), source: "ai" }); } else if (data.kind === "inventory") { const { items = [] } = data.record as { items?: InventoryInput[] }; // Commit each proposed stock item via the RBAC-gated create endpoint. for (const item of items) { await createInventory(item); } } else { await createPrescription({ ...(data.record as PrescriptionInput), source: "ai", }); } } // A structured, de-densified view of a proposed record. Inventory and invoice // records carry an item array — rendered as a compact scrollable list (one row // per item) rather than a single comma-joined wall of text; everything else // uses the per-kind summary lines. export function RecordSummary({ kind, record, }: { kind: ActionPreviewKind; record: Record; }) { const { t } = useTranslation(); if (kind === "inventory") { const items = (record.items as InventoryInput[] | undefined) ?? []; return (

{t("chat.actionCard.itemCount", { count: items.length })}

    {items.map((it, i) => (
  • {it.name} {it.strength ? ( · {it.strength} ) : null} {it.stockQuantity != null ? ( ×{it.stockQuantity} ) : null}
  • ))}
); } if (kind === "invoice") { const items = (record.lineItems as InvoiceLineItem[] | undefined) ?? []; const total = items.reduce((s, li) => s + li.quantity * li.unitPrice, 0); return (
{record.name ? (

{String(record.name)}

) : null}
    {items.map((li, i) => (
  • {li.description} ×{li.quantity} {formatMoney(li.quantity * li.unitPrice)}
  • ))}

{t("chat.actionCard.total")} {formatMoney(total)}

); } const lines = summarize({ kind, record } as ActionPreviewData); return (
{lines.map((line, i) => (

{line}

))}
); } // The human approval gate for an agent-proposed add. The agent drafts the record // (dry run, nothing written); the clinician reviews it here, may edit it, and // must approve before it is committed via the matching RBAC-gated create endpoint. export function ActionPreviewCard({ data, onResolved, }: { data: ActionPreviewData; // Called once committed/discarded so the parent can persist the resolution // (prevents re-adding after re-render or conversation reload). onResolved?: (resolution: "added" | "discarded") => void; }) { const { t } = useTranslation(); const [status, setStatus] = useState( data.resolved === "added" ? "done" : data.resolved === "discarded" ? "rejected" : "pending", ); // Editable working copy of the proposed record (edits commit, not the draft). const [record, setRecord] = useState>( data.record as Record, ); const [editOpen, setEditOpen] = useState(false); const Icon = ICONS[data.kind]; const hasIssues = (data.issues?.length ?? 0) > 0; const approve = async () => { setStatus("committing"); try { await commitAction({ ...data, record }); setStatus("done"); onResolved?.("added"); notify.success( t("chat.actionCard.addedTitle"), t(`chat.actionCard.kind.${data.kind}`), ); } catch { setStatus("pending"); notify.error( t("chat.actionCard.failedTitle"), t("chat.actionCard.failedBody"), ); } }; const editable = status === "pending"; return (
{t(`chat.actionCard.title.${data.kind}`)} {editable ? ( ) : null}
{hasIssues ? (
    {data.issues!.slice(0, 5).map((issue) => (
  • {issue}
  • ))}
) : null} {status === "done" ? (

{t("chat.actionCard.added")}

) : status === "rejected" ? (

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

) : (
)}
); }