import { useState, type ComponentProps } from "react"; import Menu from "@mui/material/Menu"; import MenuItem from "@mui/material/MenuItem"; import { StatusChip, ConfirmDialog } from "../ui"; /** One entry in a {@link StatusChipMenu}. */ export interface StatusChipAction { /** Stable identifier, e.g. the target status. */ key: string; /** Menu item text, e.g. "Dokončit". */ label: string; /** Red menu-item text + danger ConfirmDialog variant. */ danger?: boolean; /** * Confirmation dialog content. When omitted the action fires directly from * the menu without confirmation (used by "Vytvořit objednávku…", which * opens its own modal). */ confirm?: { title: string; message: string; confirmText: string }; /** * Executed on pick (after confirmation when `confirm` is set). Awaited: * while pending the ConfirmDialog shows its loading state and cannot be * closed; on resolve the dialog closes; on rejection it stays open (the * caller is responsible for toasting the error). */ onAction: () => void | Promise; } interface StatusChipMenuProps { /** Chip text (current status label). */ label: string; /** Chip color, same palette as {@link StatusChip}. */ color: ComponentProps["color"]; /** Quick actions. Empty/undefined renders a plain non-clickable chip. */ actions?: StatusChipAction[]; /** Force a plain chip (e.g. the user lacks the edit permission). */ disabled?: boolean; /** Tooltip on the clickable chip. */ title?: string; } /** * Status chip with a quick-action menu, shared by the list pages * (offers / received orders / projects). * * Clicking the chip (stopPropagation, so row navigation never fires) opens a * dense MUI Menu of the valid next actions. Picking one closes the menu and * either fires `onAction` directly (no `confirm` given) or opens the single * shared ConfirmDialog instance — danger variant for destructive actions, * `loading` while the awaited `onAction` is pending, staying open when it * rejects per app convention (the caller toasts errors). * * With no actions — or with `disabled` — it renders a plain non-clickable * StatusChip without a tooltip. */ export default function StatusChipMenu({ label, color, actions, disabled = false, title = "Změnit stav", }: StatusChipMenuProps) { const [anchorEl, setAnchorEl] = useState(null); const [pending, setPending] = useState(null); const [loading, setLoading] = useState(false); if (disabled || !actions || actions.length === 0) { return ; } const handlePick = (action: StatusChipAction) => { setAnchorEl(null); if (action.confirm) { setPending(action); } else { void (async () => { try { await action.onAction(); } catch { // Expected: the caller toasts its own errors; nothing to close here. } })(); } }; const handleConfirm = async () => { if (!pending) return; setLoading(true); try { await pending.onAction(); // Success → close; ConfirmDialog freezes its content through the fade. setPending(null); } catch { // Expected: rejection keeps the dialog open; the caller toasts the error. } finally { setLoading(false); } }; return ( <> { e.stopPropagation(); setAnchorEl(e.currentTarget); }} /> setAnchorEl(null)} anchorOrigin={{ vertical: "bottom", horizontal: "left" }} transformOrigin={{ vertical: "top", horizontal: "left" }} slotProps={{ list: { dense: true } }} > {actions.map((action) => ( { e.stopPropagation(); handlePick(action); }} sx={action.danger ? { color: "error.main" } : undefined} > {action.label} ))} setPending(null)} onConfirm={() => void handleConfirm()} title={pending?.confirm?.title ?? ""} message={pending?.confirm?.message ?? ""} confirmText={pending?.confirm?.confirmText} confirmVariant={pending?.danger ? "danger" : "primary"} loading={loading} /> ); }