539 lines
20 KiB
TypeScript

// components/DeleteRequestButton.tsx
import React, { useState, useRef, useEffect } from 'react';
import { FaTrash, FaCheckCircle, FaExclamationTriangle, FaSpinner, FaTimes, FaChevronDown, FaBan } from 'react-icons/fa';
import { useDeleteRequest, type DeleteStatus } from '../hooks/useDeleteRequest';
import { useNavigate } from 'react-router-dom';
interface DeleteRequestButtonProps {
doctype: string;
docname: string | null | undefined;
currentDeleteStatus: DeleteStatus;
userRoles: string[];
isSystemManager: boolean;
/** Called after any successful status change — use to refetch your document */
onStatusChange?: (newStatus: DeleteStatus) => void;
/** Extra classes on the button wrapper div */
className?: string;
/** If true, renders buttons inline (row). Default: column (stacked). */
inline?: boolean;
redirectOnDelete?: string;
triggerMode?: boolean; // ← ADD THIS
}
// ─── Confirmation Modal ───────────────────────────────────────────────────────
interface ConfirmModalProps {
title: string;
message: string;
confirmLabel: string;
confirmClass: string;
icon: React.ReactNode;
loading: boolean;
onConfirm: () => void;
onCancel: () => void;
}
const ConfirmModal: React.FC<ConfirmModalProps> = ({
title,
message,
confirmLabel,
confirmClass,
icon,
loading,
onConfirm,
onCancel,
}) => (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-[9999]">
<div className="bg-white dark:bg-gray-800 rounded-xl p-6 max-w-md w-full mx-4 shadow-2xl">
<div className="flex items-start gap-3 mb-4">
<span className="mt-0.5 flex-shrink-0 text-xl">{icon}</span>
<div>
<h3 className="text-base font-semibold text-gray-800 dark:text-white">{title}</h3>
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1">{message}</p>
</div>
</div>
<div className="flex justify-end gap-3">
<button
onClick={onCancel}
disabled={loading}
className="px-4 py-2 bg-gray-200 hover:bg-gray-300 dark:bg-gray-600 dark:hover:bg-gray-500 text-gray-700 dark:text-gray-200 rounded-lg text-sm font-medium disabled:opacity-50"
>
Cancel
</button>
<button
onClick={onConfirm}
disabled={loading}
className={`px-4 py-2 text-white rounded-lg text-sm font-medium flex items-center gap-2 disabled:opacity-50 ${confirmClass}`}
>
{loading ? <FaSpinner className="animate-spin" size={13} /> : null}
{confirmLabel}
</button>
</div>
</div>
</div>
);
// ─── Split Dropdown Button ────────────────────────────────────────────────────
interface SplitDropdownButtonProps {
primaryLabel: string;
primaryClass: string;
primaryIcon: React.ReactNode;
onPrimary: () => void;
secondaryLabel: string;
secondaryClass: string;
secondaryIcon: React.ReactNode;
onSecondary: () => void;
disabled?: boolean;
loading?: boolean;
}
const SplitDropdownButton: React.FC<SplitDropdownButtonProps> = ({
primaryLabel,
primaryClass,
primaryIcon,
onPrimary,
secondaryLabel,
secondaryClass,
secondaryIcon,
onSecondary,
disabled = false,
loading = false,
}) => {
const [open, setOpen] = useState(false);
const ref = useRef<HTMLDivElement>(null);
// Close on outside click
useEffect(() => {
const handler = (e: MouseEvent) => {
if (ref.current && !ref.current.contains(e.target as Node)) {
setOpen(false);
}
};
if (open) document.addEventListener('mousedown', handler);
return () => document.removeEventListener('mousedown', handler);
}, [open]);
return (
<div ref={ref} className="relative inline-flex rounded-lg overflow-visible">
{/* Primary action button */}
<button
type="button"
disabled={disabled}
onClick={onPrimary}
className={`px-4 py-2 text-white text-sm font-medium flex items-center gap-2 disabled:opacity-50 transition-colors rounded-l-lg ${primaryClass}`}
>
{loading ? <FaSpinner className="animate-spin" size={13} /> : primaryIcon}
{primaryLabel}
</button>
{/* Divider */}
<span className="w-px bg-white/30" />
{/* Chevron toggle */}
<button
type="button"
disabled={disabled}
onClick={() => setOpen((v) => !v)}
className={`px-2 py-2 text-white text-sm font-medium flex items-center disabled:opacity-50 transition-colors rounded-r-lg ${primaryClass}`}
aria-label="More options"
>
<FaChevronDown size={11} className={`transition-transform ${open ? 'rotate-180' : ''}`} />
</button>
{/* Dropdown */}
{open && (
<div className="absolute top-full left-0 mt-1 z-50 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg min-w-[160px] overflow-hidden">
<button
type="button"
disabled={disabled}
onClick={() => { setOpen(false); onSecondary(); }}
className={`w-full px-4 py-2.5 text-sm font-medium flex items-center gap-2 transition-colors ${secondaryClass}`}
>
{secondaryIcon}
{secondaryLabel}
</button>
</div>
)}
</div>
);
};
// ─── Status Badge ─────────────────────────────────────────────────────────────
const StatusBadge: React.FC<{ status: DeleteStatus }> = ({ status }) => {
if (!status) return null;
const config: Record<string, { bg: string; text: string; label: string }> = {
'Delete Request With Supervisor': {
bg: 'bg-orange-100 dark:bg-orange-900/30',
text: 'text-orange-700 dark:text-orange-300',
label: '⏳ Delete Request Pending Supervisor',
},
'Delete Request With CM': {
bg: 'bg-yellow-100 dark:bg-yellow-900/30',
text: 'text-yellow-700 dark:text-yellow-300',
label: '⏳ Delete Request Pending CM',
},
Deleted: {
bg: 'bg-red-100 dark:bg-red-900/30',
text: 'text-red-700 dark:text-red-300',
label: '🗑 Marked for Deletion',
},
};
const c = config[status as string];
if (!c) return null;
return (
<span
className={`inline-flex items-center px-2.5 py-1 rounded-full text-xs font-medium ${c.bg} ${c.text}`}
>
{c.label}
</span>
);
};
// ─── Main Component ───────────────────────────────────────────────────────────
type PendingAction =
| 'raise'
| 'supervisor_approve'
| 'supervisor_reject'
| 'cm_approve'
| 'cm_reject'
| 'direct'
| null;
const DeleteRequestButton: React.FC<DeleteRequestButtonProps> = ({
doctype,
docname,
currentDeleteStatus,
userRoles,
isSystemManager,
onStatusChange,
className = '',
inline = false,
redirectOnDelete,
triggerMode = false,
}) => {
const navigate = useNavigate();
const [pendingAction, setPendingAction] = useState<PendingAction>(null);
const [triggerOpen, setTriggerOpen] = useState(false); // ← ADD THIS
const triggerRef = useRef<HTMLDivElement>(null); // ← ADD THIS
const {
showRaiseRequest,
showApproveAsSupervisor,
showApproveAsCM,
showDirectDelete,
deleteStatus,
loading,
error,
raiseRequest,
approveAsSupervisor,
approveAsCM,
directDelete,
rejectRequest, // ← new: resets delete_status to ""
} = useDeleteRequest({
doctype,
docname,
currentDeleteStatus,
userRoles,
isSystemManager,
onSuccess: (newStatus) => {
onStatusChange?.(newStatus);
if (newStatus === 'Deleted' && redirectOnDelete) {
navigate(redirectOnDelete);
}
},
});
// Close trigger dropdown on outside click
useEffect(() => {
if (!triggerOpen) return;
const handler = (e: MouseEvent) => {
if (triggerRef.current && !triggerRef.current.contains(e.target as Node)) {
setTriggerOpen(false);
}
};
document.addEventListener('mousedown', handler);
return () => document.removeEventListener('mousedown', handler);
}, [triggerOpen]);
const handleConfirm = async () => {
if (!pendingAction) return;
const actionMap: Record<NonNullable<PendingAction>, () => Promise<void>> = {
raise: raiseRequest,
supervisor_approve: approveAsSupervisor,
supervisor_reject: rejectRequest,
cm_approve: approveAsCM,
cm_reject: rejectRequest,
direct: directDelete,
};
await actionMap[pendingAction]();
setPendingAction(null);
};
// ── Modal config per action ──────────────────────────────────────────────
const modalConfig: Record<
NonNullable<PendingAction>,
{ title: string; message: string; confirmLabel: string; confirmClass: string; icon: React.ReactNode }
> = {
raise: {
title: 'Request Deletion',
message: 'This will raise a deletion request to the Supervisor for review.',
confirmLabel: 'Raise Request',
confirmClass: 'bg-orange-600 hover:bg-orange-700',
icon: <FaExclamationTriangle className="text-orange-500" />,
},
supervisor_approve: {
title: 'Approve Deletion Request',
message: 'This will forward the deletion request to the Cluster Manager for final approval.',
confirmLabel: 'Approve & Forward',
confirmClass: 'bg-yellow-600 hover:bg-yellow-700',
icon: <FaCheckCircle className="text-yellow-500" />,
},
supervisor_reject: {
title: 'Reject Deletion Request',
message: 'This will reject the deletion request and clear the status. The record will remain active.',
confirmLabel: 'Reject Request',
confirmClass: 'bg-gray-600 hover:bg-gray-700',
icon: <FaBan className="text-gray-500" />,
},
cm_approve: {
title: 'Approve & Mark as Deleted',
message: 'This will mark the record as Deleted.',
confirmLabel: 'Approve & Delete',
confirmClass: 'bg-red-600 hover:bg-red-700',
icon: <FaTrash className="text-red-500" />,
},
cm_reject: {
title: 'Reject Deletion Request',
message: 'This will reject the deletion request and clear the status. The record will remain active.',
confirmLabel: 'Reject Request',
confirmClass: 'bg-gray-600 hover:bg-gray-700',
icon: <FaBan className="text-gray-500" />,
},
direct: {
title: 'Mark as Deleted',
message: 'This will immediately Delete The Record.',
confirmLabel: 'Delete',
confirmClass: 'bg-red-600 hover:bg-red-700',
icon: <FaTrash className="text-red-500" />,
},
};
const statusTooltip: Record<string, string> = {
'Delete Request With Supervisor': 'Delete Request Pending Supervisor',
'Delete Request With CM': 'Delete Request Pending Cluster Manager',
'Deleted': 'Marked for Deletion',
};
if (triggerMode) {
const hasRequest = !!deleteStatus;
const iconColor = hasRequest
? 'text-orange-500 dark:text-orange-400 hover:bg-orange-50 dark:hover:bg-orange-900/20'
: 'text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20';
const nothingToShow =
!showRaiseRequest && !showApproveAsSupervisor && !showApproveAsCM && !showDirectDelete && !deleteStatus;
if (nothingToShow) return null;
return (
<>
{pendingAction && (
<ConfirmModal
{...modalConfig[pendingAction]}
loading={loading}
onConfirm={handleConfirm}
onCancel={() => setPendingAction(null)}
/>
)}
<div className="relative" ref={triggerRef}>
<button
type="button"
onClick={() => setTriggerOpen((v) => !v)}
title={deleteStatus ? statusTooltip[deleteStatus as string] : 'Request Deletion'}
className={`p-2 rounded transition-colors ${iconColor}`}
disabled={loading || !docname}
>
{loading ? <FaSpinner className="animate-spin" size={14} /> : <FaTrash size={14} />}
</button>
{triggerOpen && (
<div className="absolute right-0 mt-1 z-50 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-xl p-3 min-w-[230px]">
{deleteStatus && (
<div className="mb-3">
<StatusBadge status={deleteStatus} />
</div>
)}
{error && (
<p className="text-xs text-red-500 mb-2 flex items-center gap-1">
<FaExclamationTriangle size={10} /> {error}
</p>
)}
<div className="flex flex-col gap-2">
{showRaiseRequest && (
<button
type="button"
disabled={loading || !docname}
onClick={() => { setTriggerOpen(false); setPendingAction('raise'); }}
className="w-full px-3 py-2 bg-orange-600 hover:bg-orange-700 text-white rounded-lg text-xs font-medium flex items-center gap-2 disabled:opacity-50"
>
<FaTrash size={11} /> Request Deletion
</button>
)}
{showApproveAsSupervisor && (
<>
<button
type="button"
disabled={loading || !docname}
onClick={() => { setTriggerOpen(false); setPendingAction('supervisor_approve'); }}
className="w-full px-3 py-2 bg-yellow-600 hover:bg-yellow-700 text-white rounded-lg text-xs font-medium flex items-center gap-2 disabled:opacity-50"
>
<FaCheckCircle size={11} /> Approve & Forward to CM
</button>
<button
type="button"
disabled={loading || !docname}
onClick={() => { setTriggerOpen(false); setPendingAction('supervisor_reject'); }}
className="w-full px-3 py-2 bg-gray-200 hover:bg-gray-300 dark:bg-gray-600 dark:hover:bg-gray-500 text-gray-700 dark:text-gray-200 rounded-lg text-xs font-medium flex items-center gap-2 disabled:opacity-50"
>
<FaBan size={11} /> Reject Request
</button>
</>
)}
{showApproveAsCM && (
<>
<button
type="button"
disabled={loading || !docname}
onClick={() => { setTriggerOpen(false); setPendingAction('cm_approve'); }}
className="w-full px-3 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg text-xs font-medium flex items-center gap-2 disabled:opacity-50"
>
<FaCheckCircle size={11} /> Approve & Delete
</button>
<button
type="button"
disabled={loading || !docname}
onClick={() => { setTriggerOpen(false); setPendingAction('cm_reject'); }}
className="w-full px-3 py-2 bg-gray-200 hover:bg-gray-300 dark:bg-gray-600 dark:hover:bg-gray-500 text-gray-700 dark:text-gray-200 rounded-lg text-xs font-medium flex items-center gap-2 disabled:opacity-50"
>
<FaBan size={11} /> Reject Request
</button>
</>
)}
{showDirectDelete && (
<button
type="button"
disabled={loading || !docname}
onClick={() => { setTriggerOpen(false); setPendingAction('direct'); }}
className="w-full px-3 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg text-xs font-medium flex items-center gap-2 disabled:opacity-50"
>
<FaTrash size={11} /> Delete
</button>
)}
</div>
</div>
)}
</div>
</>
);
}
const layoutClass = inline
? 'flex flex-row flex-wrap items-center gap-2'
: 'flex flex-col gap-2';
return (
<>
{/* Confirmation Modal */}
{pendingAction && (
<ConfirmModal
{...modalConfig[pendingAction]}
loading={loading}
onConfirm={handleConfirm}
onCancel={() => setPendingAction(null)}
/>
)}
<div className={`${layoutClass} ${className}`}>
{/* Status Badge — always shown when a status exists */}
{deleteStatus && <StatusBadge status={deleteStatus} />}
{/* Error */}
{error && (
<p className="text-xs text-red-500 dark:text-red-400 flex items-center gap-1">
<FaExclamationTriangle size={11} /> {error}
</p>
)}
{/* ── Raise Request Button (no reject needed here) ─────────────────── */}
{showRaiseRequest && (
<button
type="button"
disabled={loading || !docname}
onClick={() => setPendingAction('raise')}
className="px-4 py-2 bg-orange-600 hover:bg-orange-700 text-white rounded-lg text-sm font-medium flex items-center gap-2 disabled:opacity-50 transition-colors"
>
{loading ? <FaSpinner className="animate-spin" size={13} /> : <FaTrash size={13} />}
Request Deletion
</button>
)}
{/* ── Supervisor: Approve (split) + Reject ────────────────────────── */}
{showApproveAsSupervisor && (
<SplitDropdownButton
disabled={loading || !docname}
loading={loading}
// Primary — Approve & Forward
primaryLabel="Approve Request"
primaryClass="bg-yellow-600 hover:bg-yellow-700"
primaryIcon={<FaCheckCircle size={13} />}
onPrimary={() => setPendingAction('supervisor_approve')}
// Secondary — Reject
secondaryLabel="Reject Request"
secondaryClass="text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700"
secondaryIcon={<FaBan size={13} className="text-gray-500" />}
onSecondary={() => setPendingAction('supervisor_reject')}
/>
)}
{/* ── CM: Approve & Delete (split) + Reject ───────────────────────── */}
{showApproveAsCM && (
<SplitDropdownButton
disabled={loading || !docname}
loading={loading}
// Primary — Approve & Delete
primaryLabel="Approve & Delete"
primaryClass="bg-red-600 hover:bg-red-700"
primaryIcon={<FaCheckCircle size={13} />}
onPrimary={() => setPendingAction('cm_approve')}
// Secondary — Reject
secondaryLabel="Reject Request"
secondaryClass="text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700"
secondaryIcon={<FaBan size={13} className="text-gray-500" />}
onSecondary={() => setPendingAction('cm_reject')}
/>
)}
{/* ── CM / SysManager: Direct Delete (no reject needed) ───────────── */}
{showDirectDelete && (
<button
type="button"
disabled={loading || !docname}
onClick={() => setPendingAction('direct')}
className="px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg text-sm font-medium flex items-center gap-2 disabled:opacity-50 transition-colors"
>
{loading ? <FaSpinner className="animate-spin" size={13} /> : <FaTrash size={13} />}
Delete
</button>
)}
</div>
</>
);
};
export default DeleteRequestButton;