optimized condition checks
This commit is contained in:
@@ -151,8 +151,7 @@ describe('API Namespaces', () => {
|
||||
it('has all condition check methods', () => {
|
||||
const expectedMethods = [
|
||||
'submitConditionCheck',
|
||||
'getConditionChecks',
|
||||
'getConditionCheckTimeline',
|
||||
'getBatchConditionChecks',
|
||||
'getAvailableChecks',
|
||||
];
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import DeclineRentalModal from "../components/DeclineRentalModal";
|
||||
import ConditionCheckModal from "../components/ConditionCheckModal";
|
||||
import ConditionCheckViewerModal from "../components/ConditionCheckViewerModal";
|
||||
import ReturnStatusModal from "../components/ReturnStatusModal";
|
||||
import PaymentFailedModal from "../components/PaymentFailedModal";
|
||||
|
||||
const Owning: React.FC = () => {
|
||||
// Helper function to format time
|
||||
@@ -73,16 +74,27 @@ const Owning: React.FC = () => {
|
||||
const [rentalForReturn, setRentalForReturn] = useState<Rental | null>(null);
|
||||
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||
const [itemToDelete, setItemToDelete] = useState<Item | null>(null);
|
||||
const [showPaymentFailedModal, setShowPaymentFailedModal] = useState(false);
|
||||
const [paymentFailedError, setPaymentFailedError] = useState<any>(null);
|
||||
const [paymentFailedRental, setPaymentFailedRental] = useState<Rental | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetchListings();
|
||||
fetchOwnerRentals();
|
||||
fetchAvailableChecks();
|
||||
}, [user]);
|
||||
|
||||
useEffect(() => {
|
||||
if (ownerRentals.length > 0) {
|
||||
fetchConditionChecks();
|
||||
// Only fetch condition checks for rentals that will be displayed (pending/confirmed/active)
|
||||
const displayedRentals = ownerRentals.filter((r) =>
|
||||
["pending", "confirmed", "active"].includes(r.displayStatus || r.status)
|
||||
);
|
||||
if (displayedRentals.length > 0) {
|
||||
const rentalIds = displayedRentals.map((r) => r.id);
|
||||
fetchConditionChecks(displayedRentals);
|
||||
fetchAvailableChecks(rentalIds);
|
||||
} else {
|
||||
setConditionChecks([]);
|
||||
setAvailableChecks([]);
|
||||
}
|
||||
}, [ownerRentals]);
|
||||
|
||||
@@ -154,9 +166,9 @@ const Owning: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const fetchAvailableChecks = async () => {
|
||||
const fetchAvailableChecks = async (rentalIds: string[]) => {
|
||||
try {
|
||||
const response = await conditionCheckAPI.getAvailableChecks();
|
||||
const response = await conditionCheckAPI.getAvailableChecks(rentalIds);
|
||||
const checks = Array.isArray(response.data.availableChecks)
|
||||
? response.data.availableChecks
|
||||
: [];
|
||||
@@ -167,22 +179,15 @@ const Owning: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const fetchConditionChecks = async () => {
|
||||
const fetchConditionChecks = async (rentalsToFetch: Rental[]) => {
|
||||
try {
|
||||
const allChecks: ConditionCheck[] = [];
|
||||
for (const rental of ownerRentals) {
|
||||
try {
|
||||
const response = await conditionCheckAPI.getConditionChecks(
|
||||
rental.id
|
||||
);
|
||||
if (response.data.conditionChecks) {
|
||||
allChecks.push(...response.data.conditionChecks);
|
||||
}
|
||||
} catch (err) {
|
||||
// Skip rentals with no condition checks
|
||||
}
|
||||
if (rentalsToFetch.length === 0) {
|
||||
setConditionChecks([]);
|
||||
return;
|
||||
}
|
||||
setConditionChecks(allChecks);
|
||||
const rentalIds = rentalsToFetch.map((r) => r.id);
|
||||
const response = await conditionCheckAPI.getBatchConditionChecks(rentalIds);
|
||||
setConditionChecks(response.data.conditionChecks || []);
|
||||
} catch (err) {
|
||||
console.error("Failed to fetch condition checks:", err);
|
||||
setConditionChecks([]);
|
||||
@@ -208,22 +213,29 @@ const Owning: React.FC = () => {
|
||||
}
|
||||
|
||||
fetchOwnerRentals();
|
||||
fetchAvailableChecks(); // Refresh available checks after rental confirmation
|
||||
// Note: fetchAvailableChecks() removed - it will be triggered via ownerRentals useEffect
|
||||
|
||||
// Notify Navbar to update pending count
|
||||
window.dispatchEvent(new CustomEvent("rentalStatusChanged"));
|
||||
} catch (err: any) {
|
||||
console.error("Failed to accept rental request:", err);
|
||||
|
||||
// Check if it's a payment failure
|
||||
if (err.response?.data?.error?.includes("Payment failed")) {
|
||||
alert(
|
||||
`Payment failed during approval: ${
|
||||
err.response.data.details || "Unknown payment error"
|
||||
}`
|
||||
);
|
||||
// Check if it's a payment failure (HTTP 402 or payment_failed error)
|
||||
if (
|
||||
err.response?.status === 402 ||
|
||||
err.response?.data?.error === "payment_failed"
|
||||
) {
|
||||
// Find the rental to get the item name
|
||||
const rental = ownerRentals.find((r) => r.id === rentalId);
|
||||
setPaymentFailedError(err.response.data);
|
||||
setPaymentFailedRental(rental || null);
|
||||
setShowPaymentFailedModal(true);
|
||||
} else {
|
||||
alert("Failed to accept rental request");
|
||||
alert(
|
||||
err.response?.data?.error ||
|
||||
err.response?.data?.details ||
|
||||
"Failed to accept rental request"
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
setIsProcessingPayment("");
|
||||
@@ -295,8 +307,13 @@ const Owning: React.FC = () => {
|
||||
};
|
||||
|
||||
const handleConditionCheckSuccess = () => {
|
||||
fetchAvailableChecks();
|
||||
fetchConditionChecks();
|
||||
// Refetch condition checks for displayed rentals
|
||||
const displayedRentals = ownerRentals.filter((r) =>
|
||||
["pending", "confirmed", "active"].includes(r.displayStatus || r.status)
|
||||
);
|
||||
const rentalIds = displayedRentals.map((r) => r.id);
|
||||
fetchAvailableChecks(rentalIds);
|
||||
fetchConditionChecks(displayedRentals);
|
||||
};
|
||||
|
||||
const handleViewConditionCheck = (check: ConditionCheck) => {
|
||||
@@ -481,32 +498,44 @@ const Owning: React.FC = () => {
|
||||
<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>
|
||||
Confirming...
|
||||
</>
|
||||
) : processingSuccess === rental.id ? (
|
||||
<>
|
||||
<i className="bi bi-check-circle me-1"></i>
|
||||
Confirmed!
|
||||
</>
|
||||
) : (
|
||||
"Accept"
|
||||
)}
|
||||
</button>
|
||||
{rental.paymentFailedNotifiedAt &&
|
||||
(!rental.paymentMethodUpdatedAt ||
|
||||
new Date(rental.paymentFailedNotifiedAt) >
|
||||
new Date(rental.paymentMethodUpdatedAt)) ? (
|
||||
<button
|
||||
className="btn btn-sm btn-secondary"
|
||||
disabled
|
||||
>
|
||||
Waiting for Payment Update
|
||||
</button>
|
||||
) : (
|
||||
<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>
|
||||
Confirming...
|
||||
</>
|
||||
) : processingSuccess === rental.id ? (
|
||||
<>
|
||||
<i className="bi bi-check-circle me-1"></i>
|
||||
Confirmed!
|
||||
</>
|
||||
) : (
|
||||
"Accept"
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className="btn btn-sm btn-danger"
|
||||
onClick={() => handleDeclineClick(rental)}
|
||||
@@ -823,6 +852,21 @@ const Owning: React.FC = () => {
|
||||
conditionCheck={selectedConditionCheck}
|
||||
/>
|
||||
|
||||
{/* Payment Failed Modal */}
|
||||
{paymentFailedError && (
|
||||
<PaymentFailedModal
|
||||
show={showPaymentFailedModal}
|
||||
onHide={() => {
|
||||
setShowPaymentFailedModal(false);
|
||||
setPaymentFailedError(null);
|
||||
setPaymentFailedRental(null);
|
||||
fetchOwnerRentals(); // Refresh to show updated button state
|
||||
}}
|
||||
paymentError={paymentFailedError}
|
||||
itemName={paymentFailedRental?.item?.name || "Item"}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Delete Confirmation Modal */}
|
||||
{showDeleteModal && (
|
||||
<div
|
||||
|
||||
@@ -253,18 +253,14 @@ const Profile: React.FC = () => {
|
||||
(rental, index, self) => self.findIndex((r) => r.id === rental.id) === index
|
||||
);
|
||||
|
||||
const allChecks: ConditionCheck[] = [];
|
||||
for (const rental of uniqueRentals) {
|
||||
try {
|
||||
const response = await conditionCheckAPI.getConditionChecks(rental.id);
|
||||
if (response.data.conditionChecks) {
|
||||
allChecks.push(...response.data.conditionChecks);
|
||||
}
|
||||
} catch (err) {
|
||||
// Skip rentals with no condition checks
|
||||
}
|
||||
if (uniqueRentals.length === 0) {
|
||||
setConditionChecks([]);
|
||||
return;
|
||||
}
|
||||
setConditionChecks(allChecks);
|
||||
|
||||
const rentalIds = uniqueRentals.map((r) => r.id);
|
||||
const response = await conditionCheckAPI.getBatchConditionChecks(rentalIds);
|
||||
setConditionChecks(response.data.conditionChecks || []);
|
||||
} catch (err) {
|
||||
console.error("Failed to fetch condition checks:", err);
|
||||
setConditionChecks([]);
|
||||
|
||||
@@ -8,6 +8,7 @@ import ReviewItemModal from "../components/ReviewModal";
|
||||
import RentalCancellationModal from "../components/RentalCancellationModal";
|
||||
import ConditionCheckModal from "../components/ConditionCheckModal";
|
||||
import ConditionCheckViewerModal from "../components/ConditionCheckViewerModal";
|
||||
import UpdatePaymentMethodModal from "../components/UpdatePaymentMethodModal";
|
||||
|
||||
const Renting: React.FC = () => {
|
||||
// Helper function to format time
|
||||
@@ -59,15 +60,26 @@ const Renting: React.FC = () => {
|
||||
useState(false);
|
||||
const [selectedConditionCheck, setSelectedConditionCheck] =
|
||||
useState<ConditionCheck | null>(null);
|
||||
const [showUpdatePaymentModal, setShowUpdatePaymentModal] = useState(false);
|
||||
const [rentalForPaymentUpdate, setRentalForPaymentUpdate] =
|
||||
useState<Rental | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetchRentals();
|
||||
fetchAvailableChecks();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (rentals.length > 0) {
|
||||
fetchConditionChecks();
|
||||
// Only fetch condition checks for rentals that will be displayed (pending/confirmed/active)
|
||||
const displayedRentals = rentals.filter((r) =>
|
||||
["pending", "confirmed", "active"].includes(r.displayStatus || r.status)
|
||||
);
|
||||
if (displayedRentals.length > 0) {
|
||||
const rentalIds = displayedRentals.map((r) => r.id);
|
||||
fetchConditionChecks(displayedRentals);
|
||||
fetchAvailableChecks(rentalIds);
|
||||
} else {
|
||||
setConditionChecks([]);
|
||||
setAvailableChecks([]);
|
||||
}
|
||||
}, [rentals]);
|
||||
|
||||
@@ -82,9 +94,9 @@ const Renting: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const fetchAvailableChecks = async () => {
|
||||
const fetchAvailableChecks = async (rentalIds: string[]) => {
|
||||
try {
|
||||
const response = await conditionCheckAPI.getAvailableChecks();
|
||||
const response = await conditionCheckAPI.getAvailableChecks(rentalIds);
|
||||
const checks = Array.isArray(response.data.availableChecks)
|
||||
? response.data.availableChecks
|
||||
: [];
|
||||
@@ -95,25 +107,15 @@ const Renting: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const fetchConditionChecks = async () => {
|
||||
const fetchConditionChecks = async (rentalsToFetch: Rental[]) => {
|
||||
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);
|
||||
}
|
||||
if (rentalsToFetch.length === 0) {
|
||||
setConditionChecks([]);
|
||||
return;
|
||||
}
|
||||
setConditionChecks(allChecks);
|
||||
const rentalIds = rentalsToFetch.map((r) => r.id);
|
||||
const response = await conditionCheckAPI.getBatchConditionChecks(rentalIds);
|
||||
setConditionChecks(response.data.conditionChecks || []);
|
||||
} catch (err: any) {
|
||||
console.error("Failed to fetch condition checks:", err);
|
||||
setConditionChecks([]);
|
||||
@@ -136,6 +138,18 @@ const Renting: React.FC = () => {
|
||||
setRentalToCancel(null);
|
||||
};
|
||||
|
||||
const handleUpdatePaymentClick = (rental: Rental) => {
|
||||
setRentalForPaymentUpdate(rental);
|
||||
setShowUpdatePaymentModal(true);
|
||||
};
|
||||
|
||||
const handlePaymentMethodUpdated = () => {
|
||||
// Refresh rentals to get updated data
|
||||
fetchRentals();
|
||||
setShowUpdatePaymentModal(false);
|
||||
setRentalForPaymentUpdate(null);
|
||||
};
|
||||
|
||||
const handleReviewClick = (rental: Rental) => {
|
||||
setSelectedRental(rental);
|
||||
setShowReviewModal(true);
|
||||
@@ -152,8 +166,13 @@ const Renting: React.FC = () => {
|
||||
};
|
||||
|
||||
const handleConditionCheckSuccess = () => {
|
||||
fetchAvailableChecks();
|
||||
fetchConditionChecks();
|
||||
// Refetch condition checks for displayed rentals
|
||||
const displayedRentals = rentals.filter((r) =>
|
||||
["pending", "confirmed", "active"].includes(r.displayStatus || r.status)
|
||||
);
|
||||
const rentalIds = displayedRentals.map((r) => r.id);
|
||||
fetchAvailableChecks(rentalIds);
|
||||
fetchConditionChecks(displayedRentals);
|
||||
};
|
||||
|
||||
const handleViewConditionCheck = (check: ConditionCheck) => {
|
||||
@@ -362,6 +381,26 @@ const Renting: React.FC = () => {
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Payment Failed Alert */}
|
||||
{(rental.displayStatus || rental.status) === "pending" &&
|
||||
rental.paymentStatus === "pending" &&
|
||||
rental.paymentFailedNotifiedAt && (
|
||||
<div className="alert alert-warning py-2 mb-3">
|
||||
<div className="d-flex align-items-center justify-content-between">
|
||||
<small>
|
||||
<strong>Payment issue:</strong> Please update your
|
||||
payment method so the owner can approve your request.
|
||||
</small>
|
||||
<button
|
||||
className="btn btn-sm btn-warning ms-2"
|
||||
onClick={() => handleUpdatePaymentClick(rental)}
|
||||
>
|
||||
Update Payment
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="d-flex flex-column gap-2 mt-3">
|
||||
<div className="d-flex gap-2">
|
||||
{((rental.displayStatus || rental.status) === "pending" ||
|
||||
@@ -507,6 +546,20 @@ const Renting: React.FC = () => {
|
||||
}}
|
||||
conditionCheck={selectedConditionCheck}
|
||||
/>
|
||||
|
||||
{/* Update Payment Method Modal */}
|
||||
{rentalForPaymentUpdate && (
|
||||
<UpdatePaymentMethodModal
|
||||
show={showUpdatePaymentModal}
|
||||
onHide={() => {
|
||||
setShowUpdatePaymentModal(false);
|
||||
setRentalForPaymentUpdate(null);
|
||||
}}
|
||||
rentalId={rentalForPaymentUpdate.id}
|
||||
itemName={rentalForPaymentUpdate.item?.name || "Item"}
|
||||
onSuccess={handlePaymentMethodUpdated}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -240,6 +240,8 @@ export const rentalAPI = {
|
||||
startDateTime: string;
|
||||
endDateTime: string;
|
||||
}) => api.post("/rentals/cost-preview", data),
|
||||
updatePaymentMethod: (id: string, stripePaymentMethodId: string) =>
|
||||
api.put(`/rentals/${id}/payment-method`, { stripePaymentMethodId }),
|
||||
};
|
||||
|
||||
export const messageAPI = {
|
||||
@@ -335,11 +337,14 @@ export const conditionCheckAPI = {
|
||||
rentalId: string,
|
||||
data: { checkType: string; imageFilenames: string[]; notes?: string }
|
||||
) => api.post(`/condition-checks/${rentalId}`, data),
|
||||
getConditionChecks: (rentalId: string) =>
|
||||
api.get(`/condition-checks/${rentalId}`),
|
||||
getConditionCheckTimeline: (rentalId: string) =>
|
||||
api.get(`/condition-checks/${rentalId}/timeline`),
|
||||
getAvailableChecks: () => api.get("/condition-checks"),
|
||||
getBatchConditionChecks: (rentalIds: string[]) =>
|
||||
api.get(`/condition-checks/batch`, {
|
||||
params: { rentalIds: rentalIds.join(",") },
|
||||
}),
|
||||
getAvailableChecks: (rentalIds: string[]) =>
|
||||
api.get("/condition-checks", {
|
||||
params: { rentalIds: rentalIds.join(",") },
|
||||
}),
|
||||
};
|
||||
|
||||
export const feedbackAPI = {
|
||||
|
||||
Reference in New Issue
Block a user