/**
* ConfirmDialog — "are you sure", for the steps that cannot be taken back or that the next person reads:
* disqualifying a lead, losing a deal, blocking a partner. When the step needs a reason (the API refuses one
* without it), the dialog asks for it and will not confirm while it is empty.
*
* The dialog stays open while the call runs and shows the API's refusal in place, so a person never loses
* what they typed to an error.
*/
import { AlertDialog, Button, Callout, Flex, Text, TextArea } from "@radix-ui/themes";
import { useState, type ReactNode } from "react";
export interface ConfirmDialogProps {
/** The button that opens the dialog. */
trigger: ReactNode;
title: string;
description?: ReactNode;
confirmLabel: string;
tone?: "danger" | "primary";
/** Ask for a reason; the label of its field, e.g. "Why is it lost?". */
reason?: { label: string; placeholder?: string };
/** Throw to keep the dialog open with the error shown. */
onConfirm: (reason: string) => Promise<unknown> | unknown;
}
export function ConfirmDialog({ trigger, title, description, confirmLabel, tone = "primary", reason, onConfirm }: ConfirmDialogProps) {
const [open, setOpen] = useState(false);
const [text, setText] = useState("");
const [busy, setBusy] = useState(false);
const [error, setError] = useState<string | null>(null);
const blocked = !!reason && text.trim() === "";
const confirm = async () => {
setBusy(true);
setError(null);
try {
await onConfirm(text.trim());
setOpen(false);
setText("");
} catch (e) {
setError(e instanceof Error ? e.message : String(e));
} finally {
setBusy(false);
}
};
return (
<AlertDialog.Root open={open} onOpenChange={(o) => { setOpen(o); if (!o) setError(null); }}>
<AlertDialog.Trigger>{trigger}</AlertDialog.Trigger>
<AlertDialog.Content maxWidth="460px">
<AlertDialog.Title>{title}</AlertDialog.Title>
<AlertDialog.Description size="2">{description ?? "This is recorded with your name."}</AlertDialog.Description>
{reason && (
<Flex direction="column" gap="1" mt="3" asChild>
<label>
<Text size="2" weight="medium">{reason.label}</Text>
<TextArea value={text} onChange={(e) => setText(e.target.value)} placeholder={reason.placeholder} required />
</label>
</Flex>
)}
{error && (
<Callout.Root color="red" role="alert" mt="3" size="1"><Callout.Text>{error}</Callout.Text></Callout.Root>
)}
<Flex gap="3" mt="4" justify="end">
<AlertDialog.Cancel><Button variant="soft" color="gray">Cancel</Button></AlertDialog.Cancel>
<Button color={tone === "danger" ? "red" : undefined} onClick={confirm} disabled={blocked} loading={busy}>
{confirmLabel}
</Button>
</Flex>
</AlertDialog.Content>
</AlertDialog.Root>
);
}