admin can soft delete listings
This commit is contained in:
@@ -5,6 +5,7 @@ import { useAuth } from "../contexts/AuthContext";
|
||||
import { itemAPI, rentalAPI } from "../services/api";
|
||||
import GoogleMapWithRadius from "../components/GoogleMapWithRadius";
|
||||
import ItemReviews from "../components/ItemReviews";
|
||||
import ConfirmationModal from "../components/ConfirmationModal";
|
||||
|
||||
const ItemDetail: React.FC = () => {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
@@ -24,6 +25,13 @@ const ItemDetail: React.FC = () => {
|
||||
const [totalCost, setTotalCost] = useState(0);
|
||||
const [costLoading, setCostLoading] = useState(false);
|
||||
const [costError, setCostError] = useState<string | null>(null);
|
||||
const [deleteLoading, setDeleteLoading] = useState(false);
|
||||
const [deleteError, setDeleteError] = useState<string | null>(null);
|
||||
const [showConfirmModal, setShowConfirmModal] = useState(false);
|
||||
const [confirmAction, setConfirmAction] = useState<
|
||||
"delete" | "restore" | null
|
||||
>(null);
|
||||
const [deletionReason, setDeletionReason] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
fetchItem();
|
||||
@@ -68,6 +76,50 @@ const ItemDetail: React.FC = () => {
|
||||
navigate(`/items/${id}/edit`);
|
||||
};
|
||||
|
||||
const handleAdminSoftDelete = () => {
|
||||
setConfirmAction("delete");
|
||||
setShowConfirmModal(true);
|
||||
};
|
||||
|
||||
const handleAdminRestore = () => {
|
||||
setConfirmAction("restore");
|
||||
setShowConfirmModal(true);
|
||||
};
|
||||
|
||||
const handleConfirmAction = async () => {
|
||||
try {
|
||||
setDeleteLoading(true);
|
||||
setDeleteError(null);
|
||||
|
||||
if (confirmAction === "delete") {
|
||||
await itemAPI.adminSoftDeleteItem(id!, deletionReason);
|
||||
} else if (confirmAction === "restore") {
|
||||
await itemAPI.adminRestoreItem(id!);
|
||||
}
|
||||
|
||||
await fetchItem(); // Refresh the item to show updated status
|
||||
setShowConfirmModal(false);
|
||||
setConfirmAction(null);
|
||||
setDeletionReason("");
|
||||
} catch (err: any) {
|
||||
const errorMessage =
|
||||
err.response?.data?.error || `Failed to ${confirmAction} item`;
|
||||
setDeleteError(errorMessage);
|
||||
console.error(`Admin ${confirmAction} failed:`, err);
|
||||
setShowConfirmModal(false);
|
||||
setConfirmAction(null);
|
||||
setDeletionReason("");
|
||||
} finally {
|
||||
setDeleteLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancelConfirm = () => {
|
||||
setShowConfirmModal(false);
|
||||
setConfirmAction(null);
|
||||
setDeletionReason("");
|
||||
};
|
||||
|
||||
const handleRent = () => {
|
||||
const params = new URLSearchParams({
|
||||
startDate: rentalDates.startDate,
|
||||
@@ -260,17 +312,101 @@ const ItemDetail: React.FC = () => {
|
||||
}
|
||||
|
||||
const isOwner = user?.id === item.ownerId;
|
||||
const isAdmin = user?.role === "admin";
|
||||
|
||||
return (
|
||||
<div className="container mt-5">
|
||||
<div className="row justify-content-center">
|
||||
<div className="col-md-10">
|
||||
{isOwner && (
|
||||
<div className="d-flex justify-content-end mb-3">
|
||||
<button className="btn btn-outline-primary" onClick={handleEdit}>
|
||||
<i className="bi bi-pencil me-2"></i>
|
||||
Edit Listing
|
||||
</button>
|
||||
{/* Deleted Status Indicator for Admins */}
|
||||
{item.isDeleted && isAdmin && (
|
||||
<div className="alert alert-warning mb-3" role="alert">
|
||||
<i className="bi bi-exclamation-triangle-fill me-2"></i>
|
||||
<strong>Item Soft Deleted</strong> - This item is hidden from
|
||||
public listings.
|
||||
{item.deleter && (
|
||||
<span className="ms-2">
|
||||
Deleted by {item.deleter.firstName} {item.deleter.lastName}
|
||||
</span>
|
||||
)}
|
||||
{item.deletedAt && (
|
||||
<span className="ms-2">
|
||||
on {new Date(item.deletedAt).toLocaleDateString()}
|
||||
</span>
|
||||
)}
|
||||
{item.deletionReason && (
|
||||
<div className="mt-2">
|
||||
<strong>Reason:</strong> {item.deletionReason}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Delete Error Alert */}
|
||||
{deleteError && (
|
||||
<div className="alert alert-danger mb-3" role="alert">
|
||||
{deleteError}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Action Buttons (Owner Edit + Admin Soft Delete/Restore) */}
|
||||
{(isOwner || isAdmin) && (
|
||||
<div className="d-flex justify-content-end gap-2 mb-3">
|
||||
{isOwner && (
|
||||
<button
|
||||
className="btn btn-outline-primary"
|
||||
onClick={handleEdit}
|
||||
>
|
||||
<i className="bi bi-pencil me-2"></i>
|
||||
Edit Listing
|
||||
</button>
|
||||
)}
|
||||
{isAdmin && !item.isDeleted && (
|
||||
<button
|
||||
className="btn btn-outline-danger"
|
||||
onClick={handleAdminSoftDelete}
|
||||
disabled={deleteLoading}
|
||||
>
|
||||
{deleteLoading ? (
|
||||
<>
|
||||
<span
|
||||
className="spinner-border spinner-border-sm me-2"
|
||||
role="status"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
Deleting...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<i className="bi bi-trash me-2"></i>
|
||||
Delete
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
{isAdmin && item.isDeleted && (
|
||||
<button
|
||||
className="btn btn-outline-success"
|
||||
onClick={handleAdminRestore}
|
||||
disabled={deleteLoading}
|
||||
>
|
||||
{deleteLoading ? (
|
||||
<>
|
||||
<span
|
||||
className="spinner-border spinner-border-sm me-2"
|
||||
role="status"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
Restoring...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<i className="bi bi-arrow-counterclockwise me-2"></i>
|
||||
Restore
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -582,8 +718,13 @@ const ItemDetail: React.FC = () => {
|
||||
{rentalDates.startDate && rentalDates.endDate && (
|
||||
<div className="mb-3 p-2 bg-light rounded text-center">
|
||||
{costLoading ? (
|
||||
<div className="spinner-border spinner-border-sm" role="status">
|
||||
<span className="visually-hidden">Calculating...</span>
|
||||
<div
|
||||
className="spinner-border spinner-border-sm"
|
||||
role="status"
|
||||
>
|
||||
<span className="visually-hidden">
|
||||
Calculating...
|
||||
</span>
|
||||
</div>
|
||||
) : costError ? (
|
||||
<small className="text-danger">{costError}</small>
|
||||
@@ -627,6 +768,32 @@ const ItemDetail: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Confirmation Modal */}
|
||||
<ConfirmationModal
|
||||
show={showConfirmModal}
|
||||
onClose={handleCancelConfirm}
|
||||
onConfirm={handleConfirmAction}
|
||||
title={
|
||||
confirmAction === "delete" ? "Confirm Delete" : "Confirm Restore"
|
||||
}
|
||||
message={
|
||||
confirmAction === "delete"
|
||||
? "Are you sure you want to delete this item? It will be hidden from public listings."
|
||||
: "Are you sure you want to restore this item? It will be visible to the public again."
|
||||
}
|
||||
confirmText={confirmAction === "delete" ? "Delete" : "Restore"}
|
||||
cancelText="Cancel"
|
||||
confirmButtonClass={
|
||||
confirmAction === "delete" ? "btn-danger" : "btn-success"
|
||||
}
|
||||
loading={deleteLoading}
|
||||
showReasonInput={confirmAction === "delete"}
|
||||
reason={deletionReason}
|
||||
onReasonChange={setDeletionReason}
|
||||
reasonPlaceholder="Enter reason for deletion (e.g., policy violation, inappropriate content)"
|
||||
reasonRequired={true}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -533,7 +533,7 @@ const Owning: React.FC = () => {
|
||||
{item.description}
|
||||
</p>
|
||||
|
||||
<div className="mb-2">
|
||||
<div className="mb-2 d-flex gap-2 flex-wrap">
|
||||
<span
|
||||
className={`badge ${
|
||||
item.availability ? "bg-success" : "bg-secondary"
|
||||
@@ -541,6 +541,12 @@ const Owning: React.FC = () => {
|
||||
>
|
||||
{item.availability ? "Available" : "Not Available"}
|
||||
</span>
|
||||
{item.isDeleted && (
|
||||
<span className="badge bg-danger">
|
||||
<i className="bi bi-exclamation-triangle-fill me-1"></i>
|
||||
Deleted by Admin
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mb-3">
|
||||
|
||||
Reference in New Issue
Block a user