email plus return item statuses

This commit is contained in:
jackiettran
2025-10-06 15:41:48 -04:00
parent 67cc997ddc
commit 5c3d505988
28 changed files with 5861 additions and 259 deletions

View File

@@ -3,9 +3,11 @@ import { Link, useNavigate } from "react-router-dom";
import { useAuth } from "../contexts/AuthContext";
import api from "../services/api";
import { Item, Rental } from "../types";
import { rentalAPI } from "../services/api";
import { rentalAPI, conditionCheckAPI } from "../services/api";
import ReviewRenterModal from "../components/ReviewRenterModal";
import RentalCancellationModal from "../components/RentalCancellationModal";
import ConditionCheckModal from "../components/ConditionCheckModal";
import ReturnStatusModal from "../components/ReturnStatusModal";
const MyListings: React.FC = () => {
// Helper function to format time
@@ -24,8 +26,17 @@ const MyListings: React.FC = () => {
// Helper function to format date and time together
const formatDateTime = (dateTimeString: string) => {
const date = new Date(dateTimeString).toLocaleDateString();
return date;
const date = new Date(dateTimeString);
return date
.toLocaleDateString("en-US", {
month: "2-digit",
day: "2-digit",
year: "numeric",
hour: "numeric",
minute: "2-digit",
hour12: true,
})
.replace(",", "");
};
const { user } = useAuth();
@@ -42,12 +53,28 @@ const MyListings: React.FC = () => {
const [rentalToCancel, setRentalToCancel] = useState<Rental | null>(null);
const [isProcessingPayment, setIsProcessingPayment] = useState<string>("");
const [processingSuccess, setProcessingSuccess] = useState<string>("");
const [showConditionCheckModal, setShowConditionCheckModal] = useState(false);
const [conditionCheckData, setConditionCheckData] = useState<{
rental: Rental;
checkType: string;
} | null>(null);
const [availableChecks, setAvailableChecks] = useState<any[]>([]);
const [conditionChecks, setConditionChecks] = useState<any[]>([]);
const [showReturnStatusModal, setShowReturnStatusModal] = useState(false);
const [rentalForReturn, setRentalForReturn] = useState<Rental | null>(null);
useEffect(() => {
fetchMyListings();
fetchOwnerRentals();
fetchAvailableChecks();
}, [user]);
useEffect(() => {
if (ownerRentals.length > 0) {
fetchConditionChecks();
}
}, [ownerRentals]);
const fetchMyListings = async () => {
if (!user) return;
@@ -108,6 +135,44 @@ const MyListings: React.FC = () => {
}
};
const fetchAvailableChecks = async () => {
try {
const response = await conditionCheckAPI.getAvailableChecks();
const checks = Array.isArray(response.data.availableChecks)
? response.data.availableChecks
: [];
setAvailableChecks(checks);
} catch (err: any) {
console.error("Failed to fetch available checks:", err);
setAvailableChecks([]);
}
};
const fetchConditionChecks = async () => {
try {
// Fetch condition checks for all owner rentals
const allChecks: any[] = [];
for (const rental of ownerRentals) {
try {
const response = await conditionCheckAPI.getConditionChecks(
rental.id
);
const checks = Array.isArray(response.data.conditionChecks)
? response.data.conditionChecks
: [];
allChecks.push(...checks);
} catch (err) {
// Continue even if one rental fails
console.error(`Failed to fetch checks for rental ${rental.id}:`, err);
}
}
setConditionChecks(allChecks);
} catch (err: any) {
console.error("Failed to fetch condition checks:", err);
setConditionChecks([]);
}
};
// Owner functionality handlers
const handleAcceptRental = async (rentalId: string) => {
try {
@@ -127,6 +192,7 @@ const MyListings: React.FC = () => {
}
fetchOwnerRentals();
fetchAvailableChecks(); // Refresh available checks after rental confirmation
} catch (err: any) {
console.error("Failed to accept rental request:", err);
@@ -155,21 +221,27 @@ const MyListings: React.FC = () => {
}
};
const handleCompleteClick = async (rental: Rental) => {
try {
await rentalAPI.markAsCompleted(rental.id);
const handleCompleteClick = (rental: Rental) => {
setRentalForReturn(rental);
setShowReturnStatusModal(true);
};
setSelectedRentalForReview(rental);
setShowReviewRenterModal(true);
const handleReturnStatusMarked = async (updatedRental: Rental) => {
// Update the rental in the list
setOwnerRentals((prev) =>
prev.map((rental) =>
rental.id === updatedRental.id ? updatedRental : rental
)
);
fetchOwnerRentals();
} catch (err: any) {
console.error("Error marking rental as completed:", err);
alert(
"Failed to mark rental as completed: " +
(err.response?.data?.error || err.message)
);
}
// Close the return status modal
setShowReturnStatusModal(false);
setRentalForReturn(null);
// Show review modal (rental is already marked as completed by return status endpoint)
setSelectedRentalForReview(updatedRental);
setShowReviewRenterModal(true);
fetchOwnerRentals();
};
const handleReviewRenterSuccess = () => {
@@ -192,6 +264,35 @@ const MyListings: React.FC = () => {
setRentalToCancel(null);
};
const handleConditionCheck = (rental: Rental, checkType: string) => {
setConditionCheckData({ rental, checkType });
setShowConditionCheckModal(true);
};
const handleConditionCheckSuccess = () => {
fetchAvailableChecks();
fetchConditionChecks();
alert("Condition check submitted successfully!");
};
const getAvailableChecksForRental = (rentalId: string) => {
if (!Array.isArray(availableChecks)) return [];
return availableChecks.filter(
(check) =>
check.rentalId === rentalId && check.checkType === "pre_rental_owner" // Only pre-rental; post-rental is in return modal
);
};
const getCompletedChecksForRental = (rentalId: string) => {
if (!Array.isArray(conditionChecks)) return [];
return conditionChecks.filter(
(check) =>
check.rentalId === rentalId &&
(check.checkType === "pre_rental_owner" ||
check.checkType === "post_rental_owner")
);
};
// Filter owner rentals
const allOwnerRentals = ownerRentals
.filter((r) =>
@@ -331,73 +432,115 @@ const MyListings: React.FC = () => {
</div>
)}
<div className="d-flex gap-2 mt-3">
{rental.status === "pending" && (
<>
<button
className="btn btn-sm btn-success"
onClick={() => handleAcceptRental(rental.id)}
disabled={isProcessingPayment === rental.id}
>
{isProcessingPayment === rental.id ? (
<>
<div
className="spinner-border spinner-border-sm me-2"
role="status"
>
<span className="visually-hidden">
Loading...
</span>
</div>
Processing Payment...
</>
) : processingSuccess === rental.id ? (
<>
<i className="bi bi-check-circle me-1"></i>
Payment Success!
</>
) : (
"Accept"
)}
</button>
<button
className="btn btn-sm btn-danger"
onClick={() => handleRejectRental(rental.id)}
>
Reject
</button>
<button
className="btn btn-sm btn-outline-danger"
onClick={() => handleCancelClick(rental)}
>
Cancel
</button>
</>
)}
{rental.status === "confirmed" && (
<>
<div className="d-flex flex-column gap-2 mt-3">
<div className="d-flex gap-2">
{rental.status === "pending" && (
<>
<button
className="btn btn-sm btn-success"
onClick={() => handleAcceptRental(rental.id)}
disabled={isProcessingPayment === rental.id}
>
{isProcessingPayment === rental.id ? (
<>
<div
className="spinner-border spinner-border-sm me-2"
role="status"
>
<span className="visually-hidden">
Loading...
</span>
</div>
Processing Payment...
</>
) : processingSuccess === rental.id ? (
<>
<i className="bi bi-check-circle me-1"></i>
Payment Success!
</>
) : (
"Accept"
)}
</button>
<button
className="btn btn-sm btn-danger"
onClick={() => handleRejectRental(rental.id)}
>
Reject
</button>
<button
className="btn btn-sm btn-outline-danger"
onClick={() => handleCancelClick(rental)}
>
Cancel
</button>
</>
)}
{rental.status === "confirmed" && (
<>
<button
className="btn btn-sm btn-success"
onClick={() => handleCompleteClick(rental)}
>
Complete
</button>
<button
className="btn btn-sm btn-outline-danger"
onClick={() => handleCancelClick(rental)}
>
Cancel
</button>
</>
)}
{rental.status === "active" && (
<button
className="btn btn-sm btn-success"
onClick={() => handleCompleteClick(rental)}
>
Complete
</button>
<button
className="btn btn-sm btn-outline-danger"
onClick={() => handleCancelClick(rental)}
>
Cancel
</button>
</>
)}
</div>
{/* Condition Check Status */}
{getCompletedChecksForRental(rental.id).length > 0 && (
<div className="mb-2">
{getCompletedChecksForRental(rental.id).map(
(check) => (
<div
key={`${rental.id}-${check.checkType}-status`}
className="text-success small"
>
<i className="bi bi-camera-fill me-1"></i>
{check.checkType === "pre_rental_owner"
? "Pre-Rental Check Completed"
: "Post-Rental Check Completed"}
<small className="text-muted ms-2">
{new Date(
check.createdAt
).toLocaleDateString()}
</small>
</div>
)
)}
</div>
)}
{rental.status === "active" && (
{/* Condition Check Buttons */}
{getAvailableChecksForRental(rental.id).map((check) => (
<button
className="btn btn-sm btn-success"
onClick={() => handleCompleteClick(rental)}
key={`${rental.id}-${check.checkType}`}
className="btn btn-sm btn-outline-primary"
onClick={() =>
handleConditionCheck(rental, check.checkType)
}
>
Complete
<i className="bi bi-camera me-2" />
{check.checkType === "pre_rental_owner"
? "Submit Pre-Rental Check"
: "Submit Post-Rental Check"}
</button>
)}
))}
</div>
</div>
</div>
@@ -572,6 +715,35 @@ const MyListings: React.FC = () => {
onCancellationComplete={handleCancellationComplete}
/>
)}
{/* Condition Check Modal */}
{conditionCheckData && (
<ConditionCheckModal
show={showConditionCheckModal}
onHide={() => {
setShowConditionCheckModal(false);
setConditionCheckData(null);
}}
rentalId={conditionCheckData.rental.id}
checkType={conditionCheckData.checkType}
itemName={conditionCheckData.rental.item?.name || "Item"}
onSuccess={handleConditionCheckSuccess}
/>
)}
{/* Return Status Modal */}
{rentalForReturn && (
<ReturnStatusModal
show={showReturnStatusModal}
onHide={() => {
setShowReturnStatusModal(false);
setRentalForReturn(null);
}}
rental={rentalForReturn}
onReturnMarked={handleReturnStatusMarked}
onSubmitSuccess={handleReturnStatusMarked}
/>
)}
</div>
);
};

View File

@@ -1,10 +1,11 @@
import React, { useState, useEffect } from "react";
import { Link } from "react-router-dom";
import { useAuth } from "../contexts/AuthContext";
import { rentalAPI } from "../services/api";
import { rentalAPI, conditionCheckAPI } from "../services/api";
import { Rental } from "../types";
import ReviewItemModal from "../components/ReviewModal";
import RentalCancellationModal from "../components/RentalCancellationModal";
import ConditionCheckModal from "../components/ConditionCheckModal";
const MyRentals: React.FC = () => {
// Helper function to format time
@@ -21,6 +22,21 @@ const MyRentals: React.FC = () => {
}
};
// Helper function to format date and time together
const formatDateTime = (dateTimeString: string) => {
const date = new Date(dateTimeString);
return date
.toLocaleDateString("en-US", {
month: "2-digit",
day: "2-digit",
year: "numeric",
hour: "numeric",
minute: "2-digit",
hour12: true,
})
.replace(",", "");
};
const { user } = useAuth();
const [rentals, setRentals] = useState<Rental[]>([]);
const [loading, setLoading] = useState(true);
@@ -29,11 +45,25 @@ const MyRentals: React.FC = () => {
const [selectedRental, setSelectedRental] = useState<Rental | null>(null);
const [showCancelModal, setShowCancelModal] = useState(false);
const [rentalToCancel, setRentalToCancel] = useState<Rental | null>(null);
const [showConditionCheckModal, setShowConditionCheckModal] = useState(false);
const [conditionCheckData, setConditionCheckData] = useState<{
rental: Rental;
checkType: string;
} | null>(null);
const [availableChecks, setAvailableChecks] = useState<any[]>([]);
const [conditionChecks, setConditionChecks] = useState<any[]>([]);
useEffect(() => {
fetchRentals();
fetchAvailableChecks();
}, []);
useEffect(() => {
if (rentals.length > 0) {
fetchConditionChecks();
}
}, [rentals]);
const fetchRentals = async () => {
try {
const response = await rentalAPI.getMyRentals();
@@ -45,6 +75,44 @@ const MyRentals: React.FC = () => {
}
};
const fetchAvailableChecks = async () => {
try {
const response = await conditionCheckAPI.getAvailableChecks();
const checks = Array.isArray(response.data.availableChecks)
? response.data.availableChecks
: [];
setAvailableChecks(checks);
} catch (err: any) {
console.error("Failed to fetch available checks:", err);
setAvailableChecks([]);
}
};
const fetchConditionChecks = async () => {
try {
// Fetch condition checks for all rentals
const allChecks: any[] = [];
for (const rental of rentals) {
try {
const response = await conditionCheckAPI.getConditionChecks(
rental.id
);
const checks = Array.isArray(response.data.conditionChecks)
? response.data.conditionChecks
: [];
allChecks.push(...checks);
} catch (err) {
// Continue even if one rental fails
console.error(`Failed to fetch checks for rental ${rental.id}:`, err);
}
}
setConditionChecks(allChecks);
} catch (err: any) {
console.error("Failed to fetch condition checks:", err);
setConditionChecks([]);
}
};
const handleCancelClick = (rental: Rental) => {
setRentalToCancel(rental);
setShowCancelModal(true);
@@ -71,6 +139,37 @@ const MyRentals: React.FC = () => {
alert("Thank you for your review!");
};
const handleConditionCheck = (rental: Rental, checkType: string) => {
setConditionCheckData({ rental, checkType });
setShowConditionCheckModal(true);
};
const handleConditionCheckSuccess = () => {
fetchAvailableChecks();
fetchConditionChecks();
alert("Condition check submitted successfully!");
};
const getAvailableChecksForRental = (rentalId: string) => {
if (!Array.isArray(availableChecks)) return [];
return availableChecks.filter(
(check) =>
check.rentalId === rentalId &&
(check.checkType === "rental_start_renter" ||
check.checkType === "rental_end_renter")
);
};
const getCompletedChecksForRental = (rentalId: string) => {
if (!Array.isArray(conditionChecks)) return [];
return conditionChecks.filter(
(check) =>
check.rentalId === rentalId &&
(check.checkType === "rental_start_renter" ||
check.checkType === "rental_end_renter")
);
};
// Filter rentals - only show active rentals (pending, confirmed, active)
const renterActiveRentals = rentals.filter((r) =>
["pending", "confirmed", "active"].includes(r.status)
@@ -164,21 +263,13 @@ const MyRentals: React.FC = () => {
</span>
</div>
{rental.status === "pending" && (
<div className="alert alert-info mt-2 mb-2 p-2 small">
You'll only be charged if the owner approves your
request.
</div>
)}
<p className="mb-1 text-dark">
<strong>Rental Period:</strong>
<br />
<strong>Start:</strong>{" "}
{new Date(rental.startDateTime).toLocaleString()}
{formatDateTime(rental.startDateTime)}
<br />
<strong>End:</strong>{" "}
{new Date(rental.endDateTime).toLocaleString()}
<strong>End:</strong> {formatDateTime(rental.endDateTime)}
</p>
<p className="mb-1 text-dark">
@@ -237,26 +328,70 @@ const MyRentals: React.FC = () => {
</>
)}
<div className="d-flex gap-2 mt-3">
{(rental.status === "pending" ||
rental.status === "confirmed") && (
<button
className="btn btn-sm btn-danger"
onClick={() => handleCancelClick(rental)}
>
Cancel
</button>
)}
{rental.status === "active" &&
!rental.itemRating &&
!rental.itemReviewSubmittedAt && (
<div className="d-flex flex-column gap-2 mt-3">
<div className="d-flex gap-2">
{(rental.status === "pending" ||
rental.status === "confirmed") && (
<button
className="btn btn-sm btn-primary"
onClick={() => handleReviewClick(rental)}
className="btn btn-sm btn-danger"
onClick={() => handleCancelClick(rental)}
>
Review
Cancel
</button>
)}
{rental.status === "active" &&
!rental.itemRating &&
!rental.itemReviewSubmittedAt && (
<button
className="btn btn-sm btn-primary"
onClick={() => handleReviewClick(rental)}
>
Review
</button>
)}
</div>
{/* Condition Check Status */}
{getCompletedChecksForRental(rental.id).length > 0 && (
<div className="mb-2">
{getCompletedChecksForRental(rental.id).map(
(check) => (
<div
key={`${rental.id}-${check.checkType}-status`}
className="text-success small"
>
<i className="bi bi-camera-fill me-1"></i>
{check.checkType === "rental_start_renter"
? "Start Check Completed"
: "End Check Completed"}
<small className="text-muted ms-2">
{new Date(
check.createdAt
).toLocaleDateString()}
</small>
</div>
)
)}
</div>
)}
{/* Condition Check Buttons */}
{getAvailableChecksForRental(rental.id).map((check) => (
<button
key={`${rental.id}-${check.checkType}`}
className="btn btn-sm btn-outline-primary"
onClick={() =>
handleConditionCheck(rental, check.checkType)
}
>
<i className="bi bi-camera me-2" />
{check.checkType === "rental_start_renter"
? "Submit Start Check"
: "Submit End Check"}
</button>
))}
{/* Review Status */}
{rental.itemReviewSubmittedAt &&
!rental.itemReviewVisible && (
<div className="text-info small">
@@ -311,6 +446,21 @@ const MyRentals: React.FC = () => {
onCancellationComplete={handleCancellationComplete}
/>
)}
{/* Condition Check Modal */}
{conditionCheckData && (
<ConditionCheckModal
show={showConditionCheckModal}
onHide={() => {
setShowConditionCheckModal(false);
setConditionCheckData(null);
}}
rentalId={conditionCheckData.rental.id}
checkType={conditionCheckData.checkType}
itemName={conditionCheckData.rental.item?.name || "Item"}
onSuccess={handleConditionCheckSuccess}
/>
)}
</div>
);
};