import React, { useState, useEffect, useCallback } from "react"; import { rentalAPI, conditionCheckAPI } from "../services/api"; import { Rental } from "../types"; interface ReturnStatusModalProps { show: boolean; onHide: () => void; rental: Rental; onReturnMarked: (updatedRental: Rental) => void; onSubmitSuccess?: (updatedRental: Rental) => void; } const ReturnStatusModal: React.FC = ({ show, onHide, rental, onReturnMarked, onSubmitSuccess, }) => { const [statusOptions, setStatusOptions] = useState({ returned: false, returned_late: false, damaged: false, lost: false, }); const [actualReturnDateTime, setActualReturnDateTime] = useState(""); const [notes, setNotes] = useState(""); const [conditionNotes, setConditionNotes] = useState(""); const [photos, setPhotos] = useState([]); const [processing, setProcessing] = useState(false); const [error, setError] = useState(null); const [lateFeeCalculation, setLateFeeCalculation] = useState<{ lateHours: number; lateFee: number; isLate: boolean; pricingType?: "hourly" | "daily"; billableDays?: number; } | null>(null); // Damage assessment fields const [canBeFixed, setCanBeFixed] = useState(null); const [repairCost, setRepairCost] = useState(""); const [needsReplacement, setNeedsReplacement] = useState( null ); const [replacementCost, setReplacementCost] = useState(""); const [proofOfOwnership, setProofOfOwnership] = useState([]); // Initialize form when modal opens useEffect(() => { if (show && rental) { setStatusOptions({ returned: false, returned_late: false, damaged: false, lost: false, }); setActualReturnDateTime(""); setNotes(""); setConditionNotes(""); setPhotos([]); setError(null); setLateFeeCalculation(null); setCanBeFixed(null); setRepairCost(""); setNeedsReplacement(null); setReplacementCost(""); setProofOfOwnership([]); } }, [show, rental]); const formatCurrency = (amount: number | string | undefined) => { const numAmount = Number(amount) || 0; return `$${numAmount.toFixed(2)}`; }; const formatDateTime = (date: Date) => { // Format for datetime-local input in local timezone const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, "0"); const day = String(date.getDate()).padStart(2, "0"); const hours = String(date.getHours()).padStart(2, "0"); const minutes = String(date.getMinutes()).padStart(2, "0"); return `${year}-${month}-${day}T${hours}:${minutes}`; }; // Calculate late fee when actual return date/time changes useEffect(() => { const fetchLateFeeCalculation = async () => { if (statusOptions.returned_late && actualReturnDateTime && rental) { try { const response = await rentalAPI.getLateFeePreview( rental.id, actualReturnDateTime ); setLateFeeCalculation(response.data); } catch (error) { console.error("Error fetching late fee calculation:", error); setLateFeeCalculation(null); } } else { setLateFeeCalculation(null); } }; fetchLateFeeCalculation(); }, [actualReturnDateTime, statusOptions.returned_late, rental]); const handleStatusChange = ( statusType: "returned" | "returned_late" | "damaged" | "lost", checked: boolean ) => { setStatusOptions((prev) => { const newOptions = { ...prev }; // Apply the change newOptions[statusType] = checked; // Apply mutual exclusion logic if (statusType === "returned" && checked) { newOptions.returned_late = false; newOptions.lost = false; } if (statusType === "returned_late" && checked) { newOptions.returned = false; newOptions.lost = false; // Set default return time for late returns if (!actualReturnDateTime) { setActualReturnDateTime(formatDateTime(new Date())); } } if (statusType === "damaged" && checked) { newOptions.lost = false; } if (statusType === "lost" && checked) { // If item is lost, uncheck all other options newOptions.returned = false; newOptions.returned_late = false; newOptions.damaged = false; } return newOptions; }); }; const handleFileChange = (e: React.ChangeEvent) => { if (e.target.files) { const selectedFiles = Array.from(e.target.files); if (selectedFiles.length + photos.length > 20) { setError("Maximum 20 photos allowed"); return; } setPhotos((prev) => [...prev, ...selectedFiles]); setError(null); } }; const removePhoto = (index: number) => { setPhotos((prev) => prev.filter((_, i) => i !== index)); }; const handleProofOfOwnershipChange = ( e: React.ChangeEvent ) => { if (e.target.files) { const selectedFiles = Array.from(e.target.files); if (selectedFiles.length + proofOfOwnership.length > 5) { setError("Maximum 5 proof of ownership files allowed"); return; } setProofOfOwnership((prev) => [...prev, ...selectedFiles]); setError(null); } }; const removeProofOfOwnership = (index: number) => { setProofOfOwnership((prev) => prev.filter((_, i) => i !== index)); }; const handleSubmit = async () => { if (!rental) return; try { setProcessing(true); setError(null); // Check if at least one option is selected const hasSelection = Object.values(statusOptions).some( (option) => option ); if (!hasSelection) { setError("Please select at least one return status option"); return; } // Validate required fields if (statusOptions.returned_late && !actualReturnDateTime) { setError("Please provide the actual return date and time"); setProcessing(false); return; } // Validate damage assessment fields if damaged is selected if (statusOptions.damaged) { if (!conditionNotes.trim() || conditionNotes.trim().length < 10) { setError( "Please provide a detailed damage description (at least 10 characters)" ); setProcessing(false); return; } if (canBeFixed === null) { setError("Please specify if the item can be fixed"); setProcessing(false); return; } if (canBeFixed && (!repairCost || parseFloat(repairCost) <= 0)) { setError("Please provide a repair cost estimate"); setProcessing(false); return; } if (needsReplacement === null) { setError("Please specify if the item needs replacement"); setProcessing(false); return; } if ( needsReplacement && (!replacementCost || parseFloat(replacementCost) <= 0) ) { setError("Please provide a replacement cost"); setProcessing(false); return; } } let response; // If damaged is selected, use reportDamage API if (statusOptions.damaged) { const damageFormData = new FormData(); damageFormData.append("description", conditionNotes.trim()); damageFormData.append("canBeFixed", canBeFixed?.toString() || "false"); damageFormData.append( "needsReplacement", needsReplacement?.toString() || "false" ); if (canBeFixed) { damageFormData.append("repairCost", repairCost); } if (needsReplacement) { damageFormData.append("replacementCost", replacementCost); } if (actualReturnDateTime) { damageFormData.append("actualReturnDateTime", actualReturnDateTime); } // Add condition photos photos.forEach((photo) => { damageFormData.append("photos", photo); }); // Add proof of ownership files proofOfOwnership.forEach((file) => { damageFormData.append("proofOfOwnership", file); }); response = await rentalAPI.reportDamage(rental.id, damageFormData); } else { // Non-damaged returns: use existing flow // Submit post-rental condition check if photos are provided if (photos.length > 0) { const conditionCheckFormData = new FormData(); conditionCheckFormData.append("checkType", "post_rental_owner"); if (conditionNotes.trim()) { conditionCheckFormData.append("notes", conditionNotes.trim()); } photos.forEach((photo) => { conditionCheckFormData.append("photos", photo); }); await conditionCheckAPI.submitConditionCheck( rental.id, conditionCheckFormData ); } // Determine primary status for API call let primaryStatus = "returned"; if (statusOptions.returned_late) { primaryStatus = "returned_late"; } else if (statusOptions.lost) { primaryStatus = "lost"; } const data: any = { status: primaryStatus, statusOptions, // Send all selected options notes: notes.trim() || undefined, }; if (statusOptions.returned_late) { data.actualReturnDateTime = actualReturnDateTime; } response = await rentalAPI.markReturn(rental.id, data); } // Call success callback and close modal immediately if (onSubmitSuccess) { onSubmitSuccess(response.data.rental); } onHide(); } catch (error: any) { setError(error.response?.data?.error || "Failed to mark return status"); setProcessing(false); } }; const handleClose = () => { // Reset all states setStatusOptions({ returned: false, returned_late: false, damaged: false, lost: false, }); setActualReturnDateTime(""); setNotes(""); setConditionNotes(""); setPhotos([]); setError(null); setLateFeeCalculation(null); setCanBeFixed(null); setRepairCost(""); setNeedsReplacement(null); setReplacementCost(""); setProofOfOwnership([]); onHide(); }; const handleBackdropClick = useCallback( (e: React.MouseEvent) => { if (e.target === e.currentTarget) { handleClose(); } }, [handleClose] ); const handleKeyDown = useCallback( (e: KeyboardEvent) => { if (e.key === "Escape") { handleClose(); } }, [handleClose] ); useEffect(() => { if (show) { document.addEventListener("keydown", handleKeyDown); document.body.style.overflow = "hidden"; } else { document.removeEventListener("keydown", handleKeyDown); document.body.style.overflow = "unset"; } return () => { document.removeEventListener("keydown", handleKeyDown); document.body.style.overflow = "unset"; }; }, [show, handleKeyDown]); if (!show) return null; return (
Item Return
{error && (
{error}
)}
Rental Information

Item: {rental.item?.name}

Renter: {rental.renter?.firstName}{" "} {rental.renter?.lastName}

Scheduled End:
{new Date(rental.endDateTime).toLocaleString()}

handleStatusChange("returned", e.target.checked) } />
handleStatusChange("returned_late", e.target.checked) } />
handleStatusChange("damaged", e.target.checked) } />
handleStatusChange("lost", e.target.checked) } />
{statusOptions.returned_late && ( <>
When was the item actually returned to you?
setActualReturnDateTime(e.target.value)} max={formatDateTime(new Date())} required />
{actualReturnDateTime && lateFeeCalculation && (
Late Fee Calculation
{lateFeeCalculation.isLate ? ( <>
Time Overdue:{" "} {lateFeeCalculation.lateHours < 24 ? `${lateFeeCalculation.lateHours.toFixed( 1 )} hours` : `${Math.floor( lateFeeCalculation.lateHours / 24 )} days ${Math.floor( lateFeeCalculation.lateHours % 24 )} hours`}
{lateFeeCalculation.pricingType === "hourly" && (
Calculation:{" "} {lateFeeCalculation.lateHours.toFixed(1)} hours ×{" "} {formatCurrency(rental.item?.pricePerHour)} per hour
)} {lateFeeCalculation.pricingType === "daily" && lateFeeCalculation.billableDays && (
Calculation:{" "} {lateFeeCalculation.billableDays} billable day {lateFeeCalculation.billableDays > 1 ? "s" : ""}{" "} × {formatCurrency(rental.item?.pricePerDay)} per day
)}
Estimated Late Fee:{" "} {formatCurrency(lateFeeCalculation.lateFee)}
Customer service will contact the renter to confirm the late return. If the renter agrees or does not respond within 48 hours, the late fee will be charged manually. ) : (

Item was returned on time - no late fee.

)}
)} )} {!statusOptions.lost && ( <>
Document the condition of the item
{photos.length > 0 && (
{photos.map((photo, index) => (
{`Photo
{photo.name.length > 15 ? `${photo.name.substring(0, 15)}...` : photo.name}
))}
)}