145 lines
4.6 KiB
TypeScript
145 lines
4.6 KiB
TypeScript
'use client';
|
|
import { FunctionComponent, ReactNode, useState } from 'react';
|
|
import {
|
|
CircularProgress,
|
|
Dialog,
|
|
DialogActions,
|
|
DialogContent,
|
|
DialogContentText,
|
|
DialogTitle,
|
|
TextField,
|
|
} from '@mui/material';
|
|
import AppButton from '../AppButton';
|
|
|
|
export interface ConfirmDialogProps {
|
|
open: boolean;
|
|
/** Already-translated title. */
|
|
title: string;
|
|
/** Already-translated body (string or node). */
|
|
body?: ReactNode;
|
|
confirmLabel: string;
|
|
cancelLabel: string;
|
|
/** Called with the entered reason (undefined when `requireReason` is false). */
|
|
onConfirm: (reason?: string) => void;
|
|
onClose: () => void;
|
|
loading?: boolean;
|
|
/** When true a required reason field shows; confirm is disabled until it is non-empty. */
|
|
requireReason?: boolean;
|
|
reasonLabel?: string;
|
|
reasonPlaceholder?: string;
|
|
/**
|
|
* When set, confirm stays disabled until the typed value case-insensitively matches ONE of these
|
|
* (e.g. the literal word «تایید» OR the exact amount digit-string) — the guard for an irreversible
|
|
* action like running a payout batch. A gate, not a payload: `onConfirm`'s signature is unchanged, the
|
|
* typed value itself is never passed to the caller.
|
|
*/
|
|
requireTypedConfirmation?: string[];
|
|
typedConfirmationLabel?: string;
|
|
typedConfirmationPlaceholder?: string;
|
|
/** MUI color for the confirm button — `error` for a destructive action. */
|
|
confirmColor?: 'primary' | 'error' | 'secondary';
|
|
}
|
|
|
|
/**
|
|
* The shared confirmation dialog behind every audited/irreversible action — approve/reject a
|
|
* verification, run/retry a payout, save a config, resolve an alert, archive a patient, delete an
|
|
* address. Optionally collects a **required reason** (reject/hide/resolve) and disables confirm until it
|
|
* is provided. The dialog owns the reason field only; the caller owns the mutation and closes on success.
|
|
* `loading` disables the buttons and shows a spinner so a double-submit is impossible. Promoted from
|
|
* `components/admin` (kept there as a thin alias) — the contract is unchanged.
|
|
* @component ConfirmDialog
|
|
*/
|
|
const ConfirmDialog: FunctionComponent<ConfirmDialogProps> = ({
|
|
open,
|
|
title,
|
|
body,
|
|
confirmLabel,
|
|
cancelLabel,
|
|
onConfirm,
|
|
onClose,
|
|
loading = false,
|
|
requireReason = false,
|
|
reasonLabel,
|
|
reasonPlaceholder,
|
|
requireTypedConfirmation,
|
|
typedConfirmationLabel,
|
|
typedConfirmationPlaceholder,
|
|
confirmColor = 'primary',
|
|
}) => {
|
|
const [reason, setReason] = useState('');
|
|
const [typedValue, setTypedValue] = useState('');
|
|
|
|
const close = () => {
|
|
setReason('');
|
|
setTypedValue('');
|
|
onClose();
|
|
};
|
|
|
|
const confirm = () => {
|
|
onConfirm(requireReason ? reason.trim() : undefined);
|
|
setReason('');
|
|
setTypedValue('');
|
|
};
|
|
|
|
const confirmDisabled =
|
|
loading ||
|
|
(requireReason && reason.trim().length === 0) ||
|
|
(requireTypedConfirmation != null &&
|
|
!requireTypedConfirmation.some((v) => v.trim().toLowerCase() === typedValue.trim().toLowerCase()));
|
|
|
|
return (
|
|
<Dialog open={open} onClose={loading ? undefined : close} fullWidth maxWidth="xs">
|
|
<DialogTitle sx={{ fontWeight: 700 }}>{title}</DialogTitle>
|
|
<DialogContent>
|
|
{body ? (
|
|
typeof body === 'string' ? (
|
|
<DialogContentText sx={{ color: 'text.secondary' }}>{body}</DialogContentText>
|
|
) : (
|
|
body
|
|
)
|
|
) : null}
|
|
{requireReason ? (
|
|
<TextField
|
|
autoFocus
|
|
fullWidth
|
|
multiline
|
|
minRows={2}
|
|
value={reason}
|
|
onChange={(e) => setReason(e.target.value)}
|
|
label={reasonLabel}
|
|
placeholder={reasonPlaceholder}
|
|
sx={{ mt: 2 }}
|
|
/>
|
|
) : null}
|
|
{requireTypedConfirmation ? (
|
|
<TextField
|
|
autoFocus
|
|
fullWidth
|
|
value={typedValue}
|
|
onChange={(e) => setTypedValue(e.target.value)}
|
|
label={typedConfirmationLabel}
|
|
placeholder={typedConfirmationPlaceholder}
|
|
sx={{ mt: 2 }}
|
|
/>
|
|
) : null}
|
|
</DialogContent>
|
|
<DialogActions sx={{ px: 3, pb: 2 }}>
|
|
<AppButton variant="text" color="inherit" onClick={close} disabled={loading}>
|
|
{cancelLabel}
|
|
</AppButton>
|
|
<AppButton
|
|
variant="contained"
|
|
color={confirmColor}
|
|
onClick={confirm}
|
|
disabled={confirmDisabled}
|
|
startIcon={loading ? <CircularProgress size={16} color="inherit" /> : undefined}
|
|
>
|
|
{confirmLabel}
|
|
</AppButton>
|
|
</DialogActions>
|
|
</Dialog>
|
|
);
|
|
};
|
|
|
|
export default ConfirmDialog;
|