477 lines
18 KiB
JavaScript
477 lines
18 KiB
JavaScript
import React, { useState, useEffect, useMemo, useCallback, memo } from "react";
|
|
import { Link } from "react-router";
|
|
import {
|
|
XMarkIcon,
|
|
TrashIcon,
|
|
ExclamationTriangleIcon,
|
|
PencilSquareIcon,
|
|
ArrowLeftIcon,
|
|
LockClosedIcon,
|
|
ShieldExclamationIcon,
|
|
ClockIcon,
|
|
UserIcon,
|
|
GlobeAltIcon,
|
|
InformationCircleIcon,
|
|
} from "@heroicons/react/24/outline";
|
|
|
|
// Constants outside component
|
|
const GRADIENT_STYLE = {
|
|
background: "linear-gradient(to right, #8a1622, #dc2626)",
|
|
};
|
|
|
|
// Helper functions outside component
|
|
const formatDate = (dateString) => {
|
|
return dateString ? new Date(dateString).toLocaleDateString() : "N/A";
|
|
};
|
|
|
|
const formatDateTime = (dateString) => {
|
|
return dateString ? new Date(dateString).toLocaleString() : "Not available";
|
|
};
|
|
|
|
const getItemDisplayName = (item) => {
|
|
return item?.name || item?.text || "item name";
|
|
};
|
|
|
|
const getItemStatus = (status) => {
|
|
return status === 1 ? "Active" : "Inactive";
|
|
};
|
|
|
|
const DeleteConfirmationCard = memo(
|
|
({
|
|
item,
|
|
itemType,
|
|
isDeleting,
|
|
error,
|
|
confirmText,
|
|
onConfirmTextChange,
|
|
onDelete,
|
|
onCancel,
|
|
onErrorClear,
|
|
detailRoute,
|
|
editRoute,
|
|
impactWarnings = [],
|
|
alternativeText,
|
|
customFields = [],
|
|
systemInfo = true,
|
|
className = "",
|
|
}) => {
|
|
if (!item) return null;
|
|
|
|
// Memoized values
|
|
const itemDisplayName = useMemo(() => getItemDisplayName(item), [item]);
|
|
const itemStatus = useMemo(() => getItemStatus(item.status), [item.status]);
|
|
const createdDate = useMemo(
|
|
() => formatDate(item.createdAt),
|
|
[item.createdAt],
|
|
);
|
|
const createdDateTime = useMemo(
|
|
() => formatDateTime(item.createdAt),
|
|
[item.createdAt],
|
|
);
|
|
const modifiedDateTime = useMemo(
|
|
() => formatDateTime(item.modifiedAt),
|
|
[item.modifiedAt],
|
|
);
|
|
|
|
const isConfirmValid = useMemo(
|
|
() => confirmText === itemDisplayName,
|
|
[confirmText, itemDisplayName],
|
|
);
|
|
|
|
const itemTypeLower = useMemo(() => itemType.toLowerCase(), [itemType]);
|
|
|
|
// Memoized callbacks
|
|
const handleConfirmTextChange = useCallback(
|
|
(e) => {
|
|
onConfirmTextChange(e.target.value);
|
|
},
|
|
[onConfirmTextChange],
|
|
);
|
|
|
|
const handleDelete = useCallback(() => {
|
|
if (isConfirmValid && !isDeleting) {
|
|
onDelete();
|
|
}
|
|
}, [isConfirmValid, isDeleting, onDelete]);
|
|
|
|
// Memoized class strings
|
|
const inputClasses = useMemo(() => {
|
|
const baseClasses =
|
|
"w-full px-4 py-3 border-2 rounded-xl focus:ring-4 focus:outline-none transition-all duration-200 text-sm font-medium";
|
|
if (isConfirmValid) {
|
|
return `${baseClasses} border-green-400 bg-green-50 focus:ring-green-200 text-green-800`;
|
|
}
|
|
return `${baseClasses} border-red-300 bg-red-50 focus:ring-red-200 text-red-800`;
|
|
}, [isConfirmValid]);
|
|
|
|
const deleteButtonClasses = useMemo(
|
|
() =>
|
|
"inline-flex items-center justify-center px-6 py-3 text-sm font-bold text-white bg-gradient-to-r from-red-700 to-red-800 rounded-xl hover:from-red-800 hover:to-red-900 hover:shadow-xl focus:outline-none focus:ring-4 focus:ring-red-300 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200 shadow-lg",
|
|
[],
|
|
);
|
|
|
|
const cancelButtonClasses = useMemo(
|
|
() =>
|
|
"inline-flex items-center justify-center px-6 py-3 text-sm font-semibold text-gray-700 bg-white border-2 border-gray-300 rounded-xl hover:bg-gray-50 hover:border-gray-400 hover:shadow-md focus:outline-none focus:ring-4 focus:ring-gray-200 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200",
|
|
[],
|
|
);
|
|
|
|
const editLinkClasses = useMemo(
|
|
() =>
|
|
`inline-flex items-center justify-center px-6 py-3 text-sm font-semibold text-white bg-gradient-to-r from-red-500 to-red-600 rounded-xl hover:from-red-600 hover:to-red-700 hover:shadow-lg focus:outline-none focus:ring-4 focus:ring-red-200 transition-all duration-200 ${
|
|
isDeleting ? "opacity-50 pointer-events-none" : ""
|
|
}`,
|
|
[isDeleting],
|
|
);
|
|
|
|
// Memoized components
|
|
const errorSection = useMemo(() => {
|
|
if (!error) return null;
|
|
return (
|
|
<div className="mb-6 bg-red-50 border border-red-200 text-red-800 px-4 py-3 rounded-lg flex items-center justify-between">
|
|
<span className="flex items-center">
|
|
<ExclamationTriangleIcon className="w-5 h-5 mr-2" />
|
|
{error}
|
|
</span>
|
|
<button
|
|
onClick={onErrorClear}
|
|
className="text-red-600 hover:text-red-800"
|
|
>
|
|
<XMarkIcon className="w-5 h-5" />
|
|
</button>
|
|
</div>
|
|
);
|
|
}, [error, onErrorClear]);
|
|
|
|
const impactWarningSection = useMemo(() => {
|
|
if (impactWarnings.length === 0) return null;
|
|
return (
|
|
<div className="p-4 bg-amber-50 border border-amber-200 rounded-lg mb-6">
|
|
<h4 className="text-base font-medium text-amber-800 mb-3 flex items-center">
|
|
<ExclamationTriangleIcon className="w-5 h-5 mr-2" />
|
|
This deletion will affect:
|
|
</h4>
|
|
<ul className="text-sm text-amber-700 space-y-1 ml-7">
|
|
{impactWarnings.map((warning, index) => (
|
|
<li key={index} className="flex items-start">
|
|
{warning.icon && (
|
|
<warning.icon className="w-4 h-4 mr-2 flex-shrink-0 mt-0.5" />
|
|
)}
|
|
<span>{warning.text}</span>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
{alternativeText && (
|
|
<div className="mt-3 pt-3 border-t border-amber-200">
|
|
<p className="text-sm font-medium text-amber-900">
|
|
<strong>Alternative:</strong> {alternativeText}
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}, [impactWarnings, alternativeText]);
|
|
|
|
const customFieldsSection = useMemo(() => {
|
|
return customFields.map((field, index) => (
|
|
<div key={index}>
|
|
<div className="block text-base font-medium text-gray-700 mb-2">
|
|
{field.label}
|
|
</div>
|
|
<div className="p-4 bg-white rounded-xl border border-gray-200 flex items-center">
|
|
{field.icon && <field.icon className="w-5 h-5 mr-2 text-red-500" />}
|
|
<span className={field.className || "font-medium text-red-700"}>
|
|
{field.value}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
));
|
|
}, [customFields]);
|
|
|
|
const systemInfoSection = useMemo(() => {
|
|
if (!systemInfo) return null;
|
|
return (
|
|
<div className="mt-8 bg-white shadow-xl rounded-2xl overflow-hidden border border-gray-100 hover:shadow-2xl transition-shadow duration-300">
|
|
<div className="px-6 py-4 bg-blue-50 border-b border-blue-200">
|
|
<h2 className="text-lg font-bold text-blue-900 flex items-center">
|
|
<InformationCircleIcon className="w-6 h-6 mr-3 text-blue-600" />
|
|
System Information
|
|
</h2>
|
|
</div>
|
|
<div className="p-6">
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 text-sm">
|
|
<div>
|
|
<p className="font-medium text-gray-500 mb-1 flex items-center">
|
|
<ClockIcon className="w-4 h-4 mr-1 text-blue-600" />
|
|
Created At:
|
|
</p>
|
|
<p className="text-gray-900 ml-5">{createdDateTime}</p>
|
|
</div>
|
|
<div>
|
|
<p className="font-medium text-gray-500 mb-1 flex items-center">
|
|
<UserIcon className="w-4 h-4 mr-1 text-blue-600" />
|
|
Created By:
|
|
</p>
|
|
<p className="text-gray-900 ml-5">
|
|
{item.createdByUserName || "Not available"}
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<p className="font-medium text-gray-500 mb-1 flex items-center">
|
|
<ClockIcon className="w-4 h-4 mr-1 text-blue-600" />
|
|
Last Modified:
|
|
</p>
|
|
<p className="text-gray-900 ml-5">{modifiedDateTime}</p>
|
|
</div>
|
|
{item.modifiedByUserName && (
|
|
<div>
|
|
<p className="font-medium text-gray-500 mb-1 flex items-center">
|
|
<UserIcon className="w-4 h-4 mr-1 text-blue-600" />
|
|
Modified By:
|
|
</p>
|
|
<p className="text-gray-900 ml-5">
|
|
{item.modifiedByUserName}
|
|
</p>
|
|
</div>
|
|
)}
|
|
{item.createdFromIpAddress && (
|
|
<div>
|
|
<p className="font-medium text-gray-500 mb-1 flex items-center">
|
|
<GlobeAltIcon className="w-4 h-4 mr-1 text-blue-600" />
|
|
Created From IP:
|
|
</p>
|
|
<p className="text-gray-900 ml-5">
|
|
{item.createdFromIpAddress}
|
|
</p>
|
|
</div>
|
|
)}
|
|
{item.modifiedFromIpAddress && (
|
|
<div>
|
|
<p className="font-medium text-gray-500 mb-1 flex items-center">
|
|
<GlobeAltIcon className="w-4 h-4 mr-1 text-blue-600" />
|
|
Modified From IP:
|
|
</p>
|
|
<p className="text-gray-900 ml-5">
|
|
{item.modifiedFromIpAddress}
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}, [systemInfo, item, createdDateTime, modifiedDateTime]);
|
|
|
|
return (
|
|
<div className={`max-w-4xl mx-auto ${className}`}>
|
|
{/* Permanent Deletion Warning Alert */}
|
|
<div className="mb-6 bg-red-50 border-2 border-red-300 text-red-800 px-4 py-4 rounded-lg flex items-start">
|
|
<ShieldExclamationIcon className="w-6 h-6 mr-3 flex-shrink-0 mt-0.5" />
|
|
<div className="flex-1">
|
|
<p className="font-bold text-lg">Permanent Deletion Warning</p>
|
|
<p className="text-sm mt-1">
|
|
You are about to permanently delete this {itemTypeLower}. This
|
|
action cannot be undone.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="bg-white shadow-xl rounded-2xl overflow-hidden border border-gray-100 hover:shadow-2xl transition-shadow duration-300">
|
|
<div className="px-6 py-4" style={GRADIENT_STYLE}>
|
|
<h2 className="text-base font-bold text-white uppercase tracking-wider flex items-center">
|
|
<ShieldExclamationIcon className="w-5 h-5 mr-2" />
|
|
Deletion Confirmation
|
|
</h2>
|
|
</div>
|
|
|
|
<div className="p-6">
|
|
{/* Error Messages */}
|
|
{errorSection}
|
|
|
|
{/* Item Details Section */}
|
|
<div className="mb-6">
|
|
<div className="block text-base sm:text-lg font-semibold text-gray-700 mb-3 flex items-center">
|
|
{itemType} to be deleted
|
|
</div>
|
|
|
|
<div className="p-5 bg-red-50 rounded-xl border border-red-200">
|
|
<div className="space-y-4">
|
|
<div>
|
|
<div className="block text-base sm:text-lg font-semibold text-gray-700 mb-3">
|
|
Name
|
|
</div>
|
|
<div className="p-5 bg-white rounded-xl border border-gray-200 font-semibold text-lg">
|
|
{itemDisplayName}
|
|
</div>
|
|
</div>
|
|
|
|
{item.description && (
|
|
<div>
|
|
<div className="block text-base font-medium text-gray-700 mb-2">
|
|
Description
|
|
</div>
|
|
<div className="p-4 bg-white rounded-xl border border-gray-200">
|
|
{item.description}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Custom Fields */}
|
|
{customFieldsSection}
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm pt-2 border-t border-red-200">
|
|
<div>
|
|
<span className="font-medium text-gray-600">Status:</span>{" "}
|
|
<span className="text-gray-900">{itemStatus}</span>
|
|
</div>
|
|
<div>
|
|
<span className="font-medium text-gray-600">
|
|
Created:
|
|
</span>{" "}
|
|
<span className="text-gray-900">{createdDate}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Impact Warning */}
|
|
{impactWarningSection}
|
|
|
|
{/* Confirmation Section */}
|
|
<div className="p-6 bg-gradient-to-br from-red-50 via-red-25 to-white border-2 border-red-200 rounded-xl mb-6 shadow-lg">
|
|
<h4 className="text-lg font-bold text-red-800 mb-4 flex items-center">
|
|
<LockClosedIcon className="w-6 h-6 mr-3 text-red-600" />
|
|
Confirmation Required
|
|
</h4>
|
|
|
|
<div className="bg-white bg-opacity-60 backdrop-blur-sm rounded-lg p-4 mb-4 border border-red-100">
|
|
<p className="text-sm text-gray-800 mb-3 leading-relaxed">
|
|
This action will permanently remove the {itemTypeLower} from
|
|
the system. All data will be lost and cannot be recovered.
|
|
</p>
|
|
|
|
<label
|
|
htmlFor="delete-confirmation-input"
|
|
className="text-sm font-semibold text-gray-900 mb-4 block"
|
|
>
|
|
To confirm deletion, please type{" "}
|
|
<code className="px-3 py-1 bg-red-100 border border-red-300 rounded-md text-red-700 font-mono text-sm">
|
|
{itemDisplayName}
|
|
</code>{" "}
|
|
in the box below:
|
|
</label>
|
|
|
|
<div className="relative">
|
|
<input
|
|
type="text"
|
|
id="delete-confirmation-input"
|
|
name="delete-confirmation"
|
|
value={confirmText}
|
|
onChange={handleConfirmTextChange}
|
|
placeholder={`Type "${itemDisplayName}" to confirm`}
|
|
className={inputClasses}
|
|
disabled={isDeleting}
|
|
autoComplete="off"
|
|
autoFocus
|
|
/>
|
|
{isConfirmValid && (
|
|
<div className="absolute right-3 top-1/2 transform -translate-y-1/2">
|
|
<svg
|
|
className="w-5 h-5 text-green-500"
|
|
fill="currentColor"
|
|
viewBox="0 0 20 20"
|
|
>
|
|
<path
|
|
fillRule="evenodd"
|
|
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
|
|
clipRule="evenodd"
|
|
/>
|
|
</svg>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Action Buttons */}
|
|
<div className="pt-8 border-t-2 border-red-100 flex flex-col space-y-4 lg:flex-row lg:items-center lg:justify-between lg:space-y-0">
|
|
<Link
|
|
to={detailRoute}
|
|
className="inline-flex items-center text-sm font-medium text-red-600 hover:text-red-800 transition-colors duration-200"
|
|
>
|
|
<ArrowLeftIcon className="w-4 h-4 mr-2" />
|
|
Back to Detail
|
|
</Link>
|
|
|
|
<div className="flex flex-col space-y-3 lg:flex-row lg:items-center lg:space-y-0 lg:space-x-4">
|
|
<button
|
|
type="button"
|
|
onClick={onCancel}
|
|
disabled={isDeleting}
|
|
className={cancelButtonClasses}
|
|
>
|
|
<XMarkIcon className="w-4 h-4 mr-2" />
|
|
Cancel
|
|
</button>
|
|
|
|
{editRoute && (
|
|
<Link to={editRoute} className={editLinkClasses}>
|
|
<PencilSquareIcon className="w-4 h-4 mr-2" />
|
|
Edit Instead
|
|
</Link>
|
|
)}
|
|
|
|
<button
|
|
type="button"
|
|
onClick={handleDelete}
|
|
disabled={isDeleting || !isConfirmValid}
|
|
className={deleteButtonClasses}
|
|
>
|
|
{isDeleting ? (
|
|
<>
|
|
<svg
|
|
className="animate-spin -ml-1 mr-3 h-5 w-5 text-white"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<circle
|
|
className="opacity-25"
|
|
cx="12"
|
|
cy="12"
|
|
r="10"
|
|
stroke="currentColor"
|
|
strokeWidth="4"
|
|
></circle>
|
|
<path
|
|
className="opacity-75"
|
|
fill="currentColor"
|
|
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
|
></path>
|
|
</svg>
|
|
Deleting...
|
|
</>
|
|
) : (
|
|
<>
|
|
<TrashIcon className="w-5 h-5 mr-2" />
|
|
Delete Permanently
|
|
</>
|
|
)}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* System Information Card */}
|
|
{systemInfoSection}
|
|
</div>
|
|
);
|
|
},
|
|
);
|
|
|
|
// Add display name for better debugging
|
|
DeleteConfirmationCard.displayName = "DeleteConfirmationCard";
|
|
|
|
export default DeleteConfirmationCard;
|