import React, { useState, useEffect } from "react"; import { useParams, useNavigate } from "react-router-dom"; import { Item, Rental } from "../types"; import { useAuth } from "../contexts/AuthContext"; import { itemAPI, rentalAPI } from "../services/api"; import { getPublicImageUrl } from "../services/uploadService"; import GoogleMapWithRadius from "../components/GoogleMapWithRadius"; import ItemReviews from "../components/ItemReviews"; import ConfirmationModal from "../components/ConfirmationModal"; import Avatar from "../components/Avatar"; const ItemDetail: React.FC = () => { const { id } = useParams<{ id: string }>(); const navigate = useNavigate(); const { user } = useAuth(); const [item, setItem] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [selectedImage, setSelectedImage] = useState(0); const [isAlreadyRenting, setIsAlreadyRenting] = useState(false); const [rentalDates, setRentalDates] = useState({ startDate: "", startTime: "14:00", endDate: "", endTime: "12:00", }); const [totalCost, setTotalCost] = useState(0); const [costLoading, setCostLoading] = useState(false); const [costError, setCostError] = useState(null); const [deleteLoading, setDeleteLoading] = useState(false); const [deleteError, setDeleteError] = useState(null); const [showConfirmModal, setShowConfirmModal] = useState(false); const [confirmAction, setConfirmAction] = useState< "delete" | "restore" | null >(null); const [deletionReason, setDeletionReason] = useState(""); useEffect(() => { fetchItem(); }, [id]); useEffect(() => { if (user) { checkIfAlreadyRenting(); } else { setIsAlreadyRenting(false); } }, [id, user]); const fetchItem = async () => { try { const response = await itemAPI.getItem(id!); setItem(response.data); } catch (err: any) { setError(err.response?.data?.message || "Failed to fetch item"); } finally { setLoading(false); } }; const checkIfAlreadyRenting = async () => { try { const response = await rentalAPI.getRentals(); const rentals: Rental[] = response.data; // Check if user has an active rental for this item const hasActiveRental = rentals.some( (rental) => rental.item?.id === id && ["pending", "confirmed", "active"].includes(rental.status) ); setIsAlreadyRenting(hasActiveRental); } catch (err) { console.error("Failed to check rental status:", err); } }; const handleEdit = () => { 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, startTime: rentalDates.startTime, endDate: rentalDates.endDate, endTime: rentalDates.endTime, }); navigate(`/items/${id}/rent?${params.toString()}`); }; const handleCopyLink = async () => { const shareUrl = `${window.location.origin}/items/${item?.id}`; try { await navigator.clipboard.writeText(shareUrl); } catch (err) { console.error("Copy to clipboard failed:", err); } }; const handleDateTimeChange = (field: string, value: string) => { setRentalDates((prev) => ({ ...prev, [field]: value, })); }; const calculateTotalCost = async () => { if (!item || !rentalDates.startDate || !rentalDates.endDate) { setTotalCost(0); return; } try { setCostLoading(true); setCostError(null); const startDateTime = new Date( `${rentalDates.startDate}T${rentalDates.startTime}` ).toISOString(); const endDateTime = new Date( `${rentalDates.endDate}T${rentalDates.endTime}` ).toISOString(); const response = await rentalAPI.getRentalCostPreview({ itemId: item.id, startDateTime, endDateTime, }); setTotalCost(response.data.baseAmount); } catch (err: any) { setCostError(err.response?.data?.error || "Failed to calculate cost"); setTotalCost(0); } finally { setCostLoading(false); } }; const generateTimeOptions = (item: Item | null, selectedDate: string) => { const options = []; let availableAfter = "00:00"; let availableBefore = "23:59"; // Determine time constraints only if we have both item and a valid selected date if (item && selectedDate && selectedDate.trim() !== "") { const date = new Date(selectedDate); const dayName = date .toLocaleDateString("en-US", { weekday: "long" }) .toLowerCase() as | "sunday" | "monday" | "tuesday" | "wednesday" | "thursday" | "friday" | "saturday"; // Use day-specific times if available if ( item.specifyTimesPerDay && item.weeklyTimes && item.weeklyTimes[dayName] ) { const dayTimes = item.weeklyTimes[dayName]; availableAfter = dayTimes.availableAfter; availableBefore = dayTimes.availableBefore; } // Otherwise use global times else if (item.availableAfter && item.availableBefore) { availableAfter = item.availableAfter; availableBefore = item.availableBefore; } } for (let hour = 0; hour < 24; hour++) { const time24 = `${hour.toString().padStart(2, "0")}:00`; // Ensure consistent format for comparison (normalize to HH:MM) const normalizedAvailableAfter = availableAfter.length === 5 ? availableAfter : availableAfter + ":00"; const normalizedAvailableBefore = availableBefore.length === 5 ? availableBefore : availableBefore + ":00"; // Check if this time is within the available range if ( time24 >= normalizedAvailableAfter && time24 <= normalizedAvailableBefore ) { const hour12 = hour === 0 ? 12 : hour > 12 ? hour - 12 : hour; const period = hour < 12 ? "AM" : "PM"; const time12 = `${hour12}:00 ${period}`; options.push({ value: time24, label: time12 }); } } // If no options are available, return at least one option to prevent empty dropdown if (options.length === 0) { options.push({ value: "00:00", label: "Not Available" }); } return options; }; useEffect(() => { calculateTotalCost(); }, [rentalDates, item]); // Validate and adjust selected times based on item availability useEffect(() => { if (!item) return; const validateAndAdjustTime = (date: string, currentTime: string) => { if (!date) return currentTime; const availableOptions = generateTimeOptions(item, date); if (availableOptions.length === 0) return currentTime; // If current time is not in available options, use the first available time const isCurrentTimeValid = availableOptions.some( (option) => option.value === currentTime ); return isCurrentTimeValid ? currentTime : availableOptions[0].value; }; const adjustedStartTime = validateAndAdjustTime( rentalDates.startDate, rentalDates.startTime ); const adjustedEndTime = validateAndAdjustTime( rentalDates.endDate || rentalDates.startDate, rentalDates.endTime ); // Update state if times have changed if ( adjustedStartTime !== rentalDates.startTime || adjustedEndTime !== rentalDates.endTime ) { setRentalDates((prev) => ({ ...prev, startTime: adjustedStartTime, endTime: adjustedEndTime, })); } }, [item, rentalDates.startDate, rentalDates.endDate]); if (loading) { return (
Loading...
); } if (error || !item) { return (
{error || "Item not found"}
); } const isOwner = user?.id === item.ownerId; const isAdmin = user?.role === "admin"; return (
{/* Deleted Status Indicator for Admins */} {item.isDeleted && isAdmin && (
Item Soft Deleted - This item is hidden from public listings. {item.deleter && ( Deleted by {item.deleter.firstName} {item.deleter.lastName} )} {item.deletedAt && ( on {new Date(item.deletedAt).toLocaleDateString()} )} {item.deletionReason && (
Reason: {item.deletionReason}
)}
)} {/* Delete Error Alert */} {deleteError && (
{deleteError}
)} {/* Action Buttons (Owner Edit + Admin Soft Delete/Restore) */} {(isOwner || isAdmin) && (
{isOwner && ( )} {isAdmin && !item.isDeleted && ( )} {isAdmin && item.isDeleted && ( )}
)}
{/* Images */} {item.imageFilenames.length > 0 ? (
{item.name} {item.imageFilenames.length > 1 && (
{item.imageFilenames.map((image, index) => ( {`${item.name} setSelectedImage(index)} /> ))}
)}
) : (
No image available
)} {/* Item Name */}

{item.name}

{/* Owner Info */} {item.owner && (
navigate(`/users/${item.ownerId}`)} style={{ cursor: "pointer" }} > {item.owner.firstName} {item.owner.lastName}
)} {/* Description (no label) */}

{item.description}

{/* Map */} {/* Rules */} {item.rules && (
Rules

{item.rules}

)} {/* Cancellation Policy */}
Cancellation Policy
Full refund: Cancel 48+ hours before rental start time
50% refund: Cancel 24-48 hours before rental start time
No refund: Cancel within 24 hours of rental start time
Replacement Cost: ${item.replacementCost}
{/* Right Side - Sticky Pricing Card */}
{(() => { const hasAnyPositivePrice = (item.pricePerHour !== undefined && Number(item.pricePerHour) > 0) || (item.pricePerDay !== undefined && Number(item.pricePerDay) > 0) || (item.pricePerWeek !== undefined && Number(item.pricePerWeek) > 0) || (item.pricePerMonth !== undefined && Number(item.pricePerMonth) > 0); const hasAnyZeroPrice = (item.pricePerHour !== undefined && Number(item.pricePerHour) === 0) || (item.pricePerDay !== undefined && Number(item.pricePerDay) === 0) || (item.pricePerWeek !== undefined && Number(item.pricePerWeek) === 0) || (item.pricePerMonth !== undefined && Number(item.pricePerMonth) === 0); if (!hasAnyPositivePrice && hasAnyZeroPrice) { return (

Free to Borrow

); } return ( <> {item.pricePerHour !== undefined && Number(item.pricePerHour) > 0 && (

${Math.floor(Number(item.pricePerHour))}/Hour

)} {item.pricePerDay !== undefined && Number(item.pricePerDay) > 0 && (

${Math.floor(Number(item.pricePerDay))}/Day

)} {item.pricePerWeek !== undefined && Number(item.pricePerWeek) > 0 && (

${Math.floor(Number(item.pricePerWeek))}/Week

)} {item.pricePerMonth !== undefined && Number(item.pricePerMonth) > 0 && (

${Math.floor(Number(item.pricePerMonth))}/Month

)} ); })()} {/* Rental Period Selection - Only show for non-owners */} {!isOwner && item.isAvailable && !isAlreadyRenting && ( <>
handleDateTimeChange( "startDate", e.target.value ) } min={new Date().toLocaleDateString()} style={{ flex: "1 1 50%" }} />
handleDateTimeChange("endDate", e.target.value) } min={ rentalDates.startDate || new Date().toLocaleDateString() } style={{ flex: "1 1 50%" }} />
{rentalDates.startDate && rentalDates.endDate && (
{costLoading ? (
Calculating...
) : costError ? ( {costError} ) : totalCost > 0 ? ( Total: ${totalCost} ) : null}
)}
)} {/* Action Buttons */} {!isOwner && item.isAvailable && !isAlreadyRenting && (
)} {!isOwner && isAlreadyRenting && (
)}
{/* Confirmation Modal */}
); }; export default ItemDetail;