admin can soft delete listings

This commit is contained in:
jackiettran
2025-11-20 17:14:40 -05:00
parent 88c831419c
commit b2f18d77f6
11 changed files with 773 additions and 22 deletions

View File

@@ -10,6 +10,11 @@ interface ConfirmationModalProps {
cancelText?: string;
confirmButtonClass?: string;
loading?: boolean;
showReasonInput?: boolean;
reason?: string;
onReasonChange?: (reason: string) => void;
reasonPlaceholder?: string;
reasonRequired?: boolean;
}
const ConfirmationModal: React.FC<ConfirmationModalProps> = ({
@@ -21,40 +26,62 @@ const ConfirmationModal: React.FC<ConfirmationModalProps> = ({
confirmText = 'Confirm',
cancelText = 'Cancel',
confirmButtonClass = 'btn-danger',
loading = false
loading = false,
showReasonInput = false,
reason = '',
onReasonChange,
reasonPlaceholder = 'Enter reason...',
reasonRequired = false
}) => {
if (!show) return null;
const isConfirmDisabled = loading || (reasonRequired && showReasonInput && !reason.trim());
return (
<div className="modal d-block" tabIndex={-1} style={{ backgroundColor: 'rgba(0,0,0,0.5)' }}>
<div className="modal-dialog modal-dialog-centered">
<div className="modal-content">
<div className="modal-header">
<h5 className="modal-title">{title}</h5>
<button
type="button"
className="btn-close"
<button
type="button"
className="btn-close"
onClick={onClose}
disabled={loading}
></button>
</div>
<div className="modal-body">
<p>{message}</p>
{showReasonInput && (
<div className="mt-3">
<label className="form-label">
Reason {reasonRequired && <span className="text-danger">*</span>}
</label>
<textarea
className="form-control"
rows={3}
value={reason}
onChange={(e) => onReasonChange?.(e.target.value)}
placeholder={reasonPlaceholder}
disabled={loading}
/>
</div>
)}
</div>
<div className="modal-footer">
<button
type="button"
className="btn btn-secondary"
<button
type="button"
className="btn btn-secondary"
onClick={onClose}
disabled={loading}
>
{cancelText}
</button>
<button
type="button"
<button
type="button"
className={`btn ${confirmButtonClass}`}
onClick={onConfirm}
disabled={loading}
disabled={isConfirmDisabled}
>
{loading ? (
<>

View File

@@ -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>
);
};

View File

@@ -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">

View File

@@ -202,6 +202,10 @@ export const itemAPI = {
deleteItem: (id: string) => api.delete(`/items/${id}`),
getRecommendations: () => api.get("/items/recommendations"),
getItemReviews: (id: string) => api.get(`/items/${id}/reviews`),
// Admin endpoints
adminSoftDeleteItem: (id: string, reason: string) =>
api.delete(`/items/admin/${id}`, { data: { reason } }),
adminRestoreItem: (id: string) => api.patch(`/items/admin/${id}/restore`),
};
export const rentalAPI = {

View File

@@ -109,6 +109,11 @@ export interface Item {
};
ownerId: string;
owner?: User;
isDeleted?: boolean;
deletedBy?: string;
deletedAt?: string;
deletionReason?: string;
deleter?: User;
createdAt: string;
updatedAt: string;
}