955 lines
34 KiB
TypeScript
955 lines
34 KiB
TypeScript
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<ReturnStatusModalProps> = ({
|
||
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<File[]>([]);
|
||
const [processing, setProcessing] = useState(false);
|
||
const [error, setError] = useState<string | null>(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<boolean | null>(null);
|
||
const [repairCost, setRepairCost] = useState("");
|
||
const [needsReplacement, setNeedsReplacement] = useState<boolean | null>(
|
||
null
|
||
);
|
||
const [replacementCost, setReplacementCost] = useState("");
|
||
const [proofOfOwnership, setProofOfOwnership] = useState<File[]>([]);
|
||
|
||
// 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<HTMLInputElement>) => {
|
||
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<HTMLInputElement>
|
||
) => {
|
||
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 (
|
||
<div
|
||
className="modal d-block"
|
||
style={{ backgroundColor: "rgba(0,0,0,0.5)" }}
|
||
onClick={handleBackdropClick}
|
||
>
|
||
<div className="modal-dialog modal-lg modal-dialog-centered">
|
||
<div className="modal-content">
|
||
<div className="modal-header">
|
||
<h5 className="modal-title">Item Return</h5>
|
||
<button
|
||
type="button"
|
||
className="btn-close"
|
||
onClick={handleClose}
|
||
aria-label="Close"
|
||
/>
|
||
</div>
|
||
|
||
<div className="modal-body">
|
||
{error && (
|
||
<div className="alert alert-danger mb-3" role="alert">
|
||
{error}
|
||
</div>
|
||
)}
|
||
|
||
<div className="mb-4">
|
||
<h5>Rental Information</h5>
|
||
<div className="bg-light p-3 rounded">
|
||
<div className="row">
|
||
<div className="col-md-6">
|
||
<p className="mb-2">
|
||
<strong>Item:</strong> {rental.item?.name}
|
||
</p>
|
||
<p className="mb-2">
|
||
<strong>Renter:</strong> {rental.renter?.firstName}{" "}
|
||
{rental.renter?.lastName}
|
||
</p>
|
||
</div>
|
||
<div className="col-md-6">
|
||
<p className="mb-2">
|
||
<strong>Scheduled End:</strong>
|
||
<br />
|
||
{new Date(rental.endDateTime).toLocaleString()}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<form>
|
||
<div className="mb-4">
|
||
<label className="form-label">
|
||
<strong>Return Status</strong>
|
||
</label>
|
||
|
||
<div className="row g-3">
|
||
<div className="col-md-6">
|
||
<div className="form-check">
|
||
<input
|
||
className="form-check-input"
|
||
type="checkbox"
|
||
id="returned"
|
||
checked={statusOptions.returned}
|
||
onChange={(e) =>
|
||
handleStatusChange("returned", e.target.checked)
|
||
}
|
||
/>
|
||
<label className="form-check-label" htmlFor="returned">
|
||
<i className="bi bi-check-circle me-2 text-success" />
|
||
Item Returned On Time
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="col-md-6">
|
||
<div className="form-check">
|
||
<input
|
||
className="form-check-input"
|
||
type="checkbox"
|
||
id="returned_late"
|
||
checked={statusOptions.returned_late}
|
||
onChange={(e) =>
|
||
handleStatusChange("returned_late", e.target.checked)
|
||
}
|
||
/>
|
||
<label
|
||
className="form-check-label"
|
||
htmlFor="returned_late"
|
||
>
|
||
<i className="bi bi-clock me-2 text-warning" />
|
||
Item Returned Late
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="col-md-6">
|
||
<div className="form-check">
|
||
<input
|
||
className="form-check-input"
|
||
type="checkbox"
|
||
id="damaged"
|
||
checked={statusOptions.damaged}
|
||
onChange={(e) =>
|
||
handleStatusChange("damaged", e.target.checked)
|
||
}
|
||
/>
|
||
<label className="form-check-label" htmlFor="damaged">
|
||
<i className="bi bi-exclamation-triangle me-2 text-warning" />
|
||
Item Damaged
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="col-md-6">
|
||
<div className="form-check">
|
||
<input
|
||
className="form-check-input"
|
||
type="checkbox"
|
||
id="lost"
|
||
checked={statusOptions.lost}
|
||
onChange={(e) =>
|
||
handleStatusChange("lost", e.target.checked)
|
||
}
|
||
/>
|
||
<label className="form-check-label" htmlFor="lost">
|
||
<i className="bi bi-x-circle me-2 text-danger" />
|
||
Item Lost
|
||
</label>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{statusOptions.returned_late && (
|
||
<>
|
||
<div className="mb-4">
|
||
<label
|
||
className="form-label"
|
||
htmlFor="actualReturnDateTime"
|
||
>
|
||
<strong>Actual Return Date & Time</strong>
|
||
</label>
|
||
<div className="form-text mb-2">
|
||
When was the item actually returned to you?
|
||
</div>
|
||
<input
|
||
type="datetime-local"
|
||
id="actualReturnDateTime"
|
||
className="form-control"
|
||
value={actualReturnDateTime}
|
||
onChange={(e) => setActualReturnDateTime(e.target.value)}
|
||
max={formatDateTime(new Date())}
|
||
required
|
||
/>
|
||
</div>
|
||
|
||
{actualReturnDateTime && lateFeeCalculation && (
|
||
<div className="alert alert-warning mb-4">
|
||
<h6>
|
||
<i className="bi bi-exclamation-circle me-2" />
|
||
Late Fee Calculation
|
||
</h6>
|
||
{lateFeeCalculation.isLate ? (
|
||
<>
|
||
<div className="mb-2">
|
||
<strong>Time Overdue:</strong>{" "}
|
||
{lateFeeCalculation.lateHours < 24
|
||
? `${lateFeeCalculation.lateHours.toFixed(
|
||
1
|
||
)} hours`
|
||
: `${Math.floor(
|
||
lateFeeCalculation.lateHours / 24
|
||
)} days ${Math.floor(
|
||
lateFeeCalculation.lateHours % 24
|
||
)} hours`}
|
||
</div>
|
||
{lateFeeCalculation.pricingType === "hourly" && (
|
||
<div className="mb-2">
|
||
<strong>Calculation:</strong>{" "}
|
||
{lateFeeCalculation.lateHours.toFixed(1)} hours ×{" "}
|
||
{formatCurrency(rental.item?.pricePerHour)} per
|
||
hour
|
||
</div>
|
||
)}
|
||
{lateFeeCalculation.pricingType === "daily" &&
|
||
lateFeeCalculation.billableDays && (
|
||
<div className="mb-2">
|
||
<strong>Calculation:</strong>{" "}
|
||
{lateFeeCalculation.billableDays} billable day
|
||
{lateFeeCalculation.billableDays > 1
|
||
? "s"
|
||
: ""}{" "}
|
||
× {formatCurrency(rental.item?.pricePerDay)} per
|
||
day
|
||
</div>
|
||
)}
|
||
<div className="mb-2">
|
||
<strong>Estimated Late Fee:</strong>{" "}
|
||
{formatCurrency(lateFeeCalculation.lateFee)}
|
||
</div>
|
||
<small className="text-muted">
|
||
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.
|
||
</small>
|
||
</>
|
||
) : (
|
||
<p className="mb-0 text-success">
|
||
<i className="bi bi-check-circle me-2" />
|
||
Item was returned on time - no late fee.
|
||
</p>
|
||
)}
|
||
</div>
|
||
)}
|
||
</>
|
||
)}
|
||
|
||
{!statusOptions.lost && (
|
||
<>
|
||
<div className="mb-4">
|
||
<label className="form-label">
|
||
<strong>Post-Rental Condition</strong>{" "}
|
||
<small className="text-muted ms-2">(Optional)</small>
|
||
</label>
|
||
<div className="form-text mb-2">
|
||
Document the condition of the item
|
||
</div>
|
||
<input
|
||
type="file"
|
||
className="form-control"
|
||
multiple
|
||
accept="image/*"
|
||
onChange={handleFileChange}
|
||
disabled={processing}
|
||
/>
|
||
</div>
|
||
|
||
{photos.length > 0 && (
|
||
<div className="mb-4">
|
||
<label className="form-label">
|
||
Selected Photos ({photos.length})
|
||
</label>
|
||
<div className="row">
|
||
{photos.map((photo, index) => (
|
||
<div key={index} className="col-md-3 mb-2">
|
||
<div className="position-relative">
|
||
<img
|
||
src={URL.createObjectURL(photo)}
|
||
alt={`Photo ${index + 1}`}
|
||
className="img-fluid rounded"
|
||
style={{
|
||
height: "100px",
|
||
objectFit: "cover",
|
||
width: "100%",
|
||
}}
|
||
/>
|
||
<button
|
||
type="button"
|
||
className="btn btn-danger btn-sm position-absolute top-0 end-0"
|
||
onClick={() => removePhoto(index)}
|
||
disabled={processing}
|
||
style={{ transform: "translate(50%, -50%)" }}
|
||
>
|
||
<i className="bi bi-x" />
|
||
</button>
|
||
</div>
|
||
<small className="text-muted d-block text-center mt-1">
|
||
{photo.name.length > 15
|
||
? `${photo.name.substring(0, 15)}...`
|
||
: photo.name}
|
||
</small>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
<div className="mb-4">
|
||
<label className="form-label" htmlFor="conditionNotes">
|
||
<strong>
|
||
{statusOptions.damaged
|
||
? "Damage Description"
|
||
: "Condition Notes"}
|
||
</strong>
|
||
{statusOptions.damaged ? (
|
||
<span className="text-danger ms-1">*</span>
|
||
) : (
|
||
<span className="text-muted ms-1">(Optional)</span>
|
||
)}
|
||
</label>
|
||
<textarea
|
||
id="conditionNotes"
|
||
className="form-control"
|
||
rows={3}
|
||
value={conditionNotes}
|
||
onChange={(e) => setConditionNotes(e.target.value)}
|
||
placeholder={
|
||
statusOptions.damaged
|
||
? "Describe the damage in detail..."
|
||
: "Add any notes about the item's condition..."
|
||
}
|
||
maxLength={500}
|
||
disabled={processing}
|
||
required={statusOptions.damaged}
|
||
/>
|
||
<div className="form-text">
|
||
{conditionNotes.length}/500 characters
|
||
{statusOptions.damaged && " (minimum 10 required)"}
|
||
</div>
|
||
</div>
|
||
</>
|
||
)}
|
||
|
||
{statusOptions.lost && (
|
||
<div className="mb-4">
|
||
<div className="alert alert-danger">
|
||
<h6>
|
||
<i className="bi bi-exclamation-triangle me-2" />
|
||
Lost Item Claim
|
||
</h6>
|
||
<div className="mb-3">
|
||
<strong>Replacement Cost:</strong>{" "}
|
||
{formatCurrency(rental.item?.replacementCost)}
|
||
</div>
|
||
<p className="mb-2">
|
||
Customer service will review this lost item claim and
|
||
contact both you and the renter.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{statusOptions.damaged && (
|
||
<div className="mb-4">
|
||
<div className="alert alert-warning">
|
||
<div className="mb-3">
|
||
<label className="form-label">
|
||
<strong>Can item be fixed?</strong>
|
||
</label>
|
||
<div>
|
||
<div className="form-check form-check-inline">
|
||
<input
|
||
className="form-check-input"
|
||
type="radio"
|
||
name="canBeFixed"
|
||
id="canBeFixedYes"
|
||
checked={canBeFixed === true}
|
||
onChange={() => setCanBeFixed(true)}
|
||
disabled={processing}
|
||
/>
|
||
<label
|
||
className="form-check-label"
|
||
htmlFor="canBeFixedYes"
|
||
>
|
||
Yes
|
||
</label>
|
||
</div>
|
||
<div className="form-check form-check-inline">
|
||
<input
|
||
className="form-check-input"
|
||
type="radio"
|
||
name="canBeFixed"
|
||
id="canBeFixedNo"
|
||
checked={canBeFixed === false}
|
||
onChange={() => setCanBeFixed(false)}
|
||
disabled={processing}
|
||
/>
|
||
<label
|
||
className="form-check-label"
|
||
htmlFor="canBeFixedNo"
|
||
>
|
||
No
|
||
</label>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{canBeFixed === true && (
|
||
<div className="mb-3">
|
||
<label className="form-label" htmlFor="repairCost">
|
||
<strong>Repair Cost</strong>{" "}
|
||
<span className="text-danger">*</span>
|
||
</label>
|
||
<div className="input-group">
|
||
<span className="input-group-text">$</span>
|
||
<input
|
||
type="number"
|
||
id="repairCost"
|
||
className="form-control"
|
||
value={repairCost}
|
||
onChange={(e) => setRepairCost(e.target.value)}
|
||
placeholder="0.00"
|
||
min="0"
|
||
step="0.01"
|
||
disabled={processing}
|
||
required
|
||
/>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
<div className="mb-3">
|
||
<label className="form-label">
|
||
<strong>Does item need replacement?</strong>
|
||
</label>
|
||
<div>
|
||
<div className="form-check form-check-inline">
|
||
<input
|
||
className="form-check-input"
|
||
type="radio"
|
||
name="needsReplacement"
|
||
id="needsReplacementYes"
|
||
checked={needsReplacement === true}
|
||
onChange={() => setNeedsReplacement(true)}
|
||
disabled={processing}
|
||
/>
|
||
<label
|
||
className="form-check-label"
|
||
htmlFor="needsReplacementYes"
|
||
>
|
||
Yes
|
||
</label>
|
||
</div>
|
||
<div className="form-check form-check-inline">
|
||
<input
|
||
className="form-check-input"
|
||
type="radio"
|
||
name="needsReplacement"
|
||
id="needsReplacementNo"
|
||
checked={needsReplacement === false}
|
||
onChange={() => setNeedsReplacement(false)}
|
||
disabled={processing}
|
||
/>
|
||
<label
|
||
className="form-check-label"
|
||
htmlFor="needsReplacementNo"
|
||
>
|
||
No
|
||
</label>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{needsReplacement === true && (
|
||
<div className="mb-3">
|
||
<label className="form-label" htmlFor="replacementCost">
|
||
<strong>Replacement Cost</strong>{" "}
|
||
<span className="text-danger">*</span>
|
||
</label>
|
||
<div className="input-group">
|
||
<span className="input-group-text">$</span>
|
||
<input
|
||
type="number"
|
||
id="replacementCost"
|
||
className="form-control"
|
||
value={replacementCost}
|
||
onChange={(e) => setReplacementCost(e.target.value)}
|
||
placeholder="0.00"
|
||
min="0"
|
||
step="0.01"
|
||
disabled={processing}
|
||
required
|
||
/>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
<div className="mb-3">
|
||
<label className="form-label">
|
||
<strong>Proof of Ownership</strong>{" "}
|
||
<small className="text-muted">(Optional)</small>
|
||
</label>
|
||
<div className="form-text mb-2">
|
||
Upload receipts, invoices, or other documents showing
|
||
proof of ownership
|
||
</div>
|
||
<input
|
||
type="file"
|
||
className="form-control"
|
||
multiple
|
||
accept="image/*,.pdf"
|
||
onChange={handleProofOfOwnershipChange}
|
||
disabled={processing}
|
||
/>
|
||
</div>
|
||
|
||
{proofOfOwnership.length > 0 && (
|
||
<div className="mb-3">
|
||
<label className="form-label">
|
||
Proof of Ownership Files ({proofOfOwnership.length})
|
||
</label>
|
||
<ul className="list-group">
|
||
{proofOfOwnership.map((file, index) => (
|
||
<li
|
||
key={index}
|
||
className="list-group-item d-flex justify-content-between align-items-center"
|
||
>
|
||
<span>
|
||
<i className="bi bi-file-earmark me-2" />
|
||
{file.name.length > 40
|
||
? `${file.name.substring(0, 40)}...`
|
||
: file.name}
|
||
</span>
|
||
<button
|
||
type="button"
|
||
className="btn btn-sm btn-danger"
|
||
onClick={() => removeProofOfOwnership(index)}
|
||
disabled={processing}
|
||
>
|
||
<i className="bi bi-x" />
|
||
</button>
|
||
</li>
|
||
))}
|
||
</ul>
|
||
</div>
|
||
)}
|
||
|
||
<small className="text-muted">
|
||
<i className="bi bi-info-circle me-1" />
|
||
Customer service will review this damage claim and contact
|
||
you if additional information is needed.
|
||
</small>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</form>
|
||
</div>
|
||
|
||
<div className="modal-footer">
|
||
<button
|
||
type="button"
|
||
className="btn btn-secondary"
|
||
onClick={handleClose}
|
||
disabled={processing}
|
||
>
|
||
Cancel
|
||
</button>
|
||
|
||
<button
|
||
type="button"
|
||
className="btn btn-primary"
|
||
onClick={handleSubmit}
|
||
disabled={
|
||
processing ||
|
||
(statusOptions.returned_late && !actualReturnDateTime)
|
||
}
|
||
>
|
||
{processing ? (
|
||
<>
|
||
<div
|
||
className="spinner-border spinner-border-sm me-2"
|
||
role="status"
|
||
>
|
||
<span className="visually-hidden">Loading...</span>
|
||
</div>
|
||
Processing...
|
||
</>
|
||
) : (
|
||
"Submit"
|
||
)}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default ReturnStatusModal;
|