refund and delayed charge
This commit is contained in:
@@ -5,6 +5,7 @@ import api from "../services/api";
|
||||
import { Item, Rental } from "../types";
|
||||
import { rentalAPI } from "../services/api";
|
||||
import ReviewRenterModal from "../components/ReviewRenterModal";
|
||||
import RentalCancellationModal from "../components/RentalCancellationModal";
|
||||
|
||||
const MyListings: React.FC = () => {
|
||||
// Helper function to format time
|
||||
@@ -37,6 +38,10 @@ const MyListings: React.FC = () => {
|
||||
const [showReviewRenterModal, setShowReviewRenterModal] = useState(false);
|
||||
const [selectedRentalForReview, setSelectedRentalForReview] =
|
||||
useState<Rental | null>(null);
|
||||
const [showCancelModal, setShowCancelModal] = useState(false);
|
||||
const [rentalToCancel, setRentalToCancel] = useState<Rental | null>(null);
|
||||
const [isProcessingPayment, setIsProcessingPayment] = useState<string>("");
|
||||
const [processingSuccess, setProcessingSuccess] = useState<string>("");
|
||||
|
||||
useEffect(() => {
|
||||
fetchMyListings();
|
||||
@@ -106,11 +111,37 @@ const MyListings: React.FC = () => {
|
||||
// Owner functionality handlers
|
||||
const handleAcceptRental = async (rentalId: string) => {
|
||||
try {
|
||||
await rentalAPI.updateRentalStatus(rentalId, "confirmed");
|
||||
setIsProcessingPayment(rentalId);
|
||||
const response = await rentalAPI.updateRentalStatus(
|
||||
rentalId,
|
||||
"confirmed"
|
||||
);
|
||||
|
||||
// Check if payment processing was successful
|
||||
if (response.data.paymentStatus === "paid") {
|
||||
// Payment successful, rental confirmed
|
||||
setProcessingSuccess(rentalId);
|
||||
setTimeout(() => {
|
||||
setProcessingSuccess("");
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
fetchOwnerRentals();
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
console.error("Failed to accept rental request:", err);
|
||||
alert("Failed to accept rental request");
|
||||
|
||||
// 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"
|
||||
}`
|
||||
);
|
||||
} else {
|
||||
alert("Failed to accept rental request");
|
||||
}
|
||||
} finally {
|
||||
setIsProcessingPayment("");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -145,9 +176,27 @@ const MyListings: React.FC = () => {
|
||||
fetchOwnerRentals();
|
||||
};
|
||||
|
||||
const handleCancelClick = (rental: Rental) => {
|
||||
setRentalToCancel(rental);
|
||||
setShowCancelModal(true);
|
||||
};
|
||||
|
||||
const handleCancellationComplete = (updatedRental: Rental) => {
|
||||
// Update the rental in the owner rentals list
|
||||
setOwnerRentals((prev) =>
|
||||
prev.map((rental) =>
|
||||
rental.id === updatedRental.id ? updatedRental : rental
|
||||
)
|
||||
);
|
||||
setShowCancelModal(false);
|
||||
setRentalToCancel(null);
|
||||
};
|
||||
|
||||
// Filter owner rentals
|
||||
const allOwnerRentals = ownerRentals
|
||||
.filter((r) => ["pending", "confirmed", "active"].includes(r.status))
|
||||
.filter((r) =>
|
||||
["pending", "confirmed", "active", "cancelled"].includes(r.status)
|
||||
)
|
||||
.sort((a, b) => {
|
||||
const statusOrder = { pending: 0, confirmed: 1, active: 2 };
|
||||
return (
|
||||
@@ -242,6 +291,35 @@ const MyListings: React.FC = () => {
|
||||
<strong>Total:</strong> ${rental.totalAmount}
|
||||
</p>
|
||||
|
||||
{rental.status === "cancelled" &&
|
||||
rental.refundAmount !== undefined && (
|
||||
<div className="alert alert-info mt-2 mb-2 p-2 small">
|
||||
<strong>
|
||||
<i className="bi bi-arrow-return-left me-1"></i>
|
||||
Refund:
|
||||
</strong>{" "}
|
||||
${Number(rental.refundAmount || 0).toFixed(2)}
|
||||
{rental.refundProcessedAt && (
|
||||
<small className="d-block text-muted mt-1">
|
||||
Processed:{" "}
|
||||
{new Date(
|
||||
rental.refundProcessedAt
|
||||
).toLocaleDateString()}
|
||||
</small>
|
||||
)}
|
||||
{rental.refundReason && (
|
||||
<small className="d-block mt-1">
|
||||
{rental.refundReason}
|
||||
</small>
|
||||
)}
|
||||
{rental.cancelledBy && (
|
||||
<small className="d-block text-muted mt-1">
|
||||
Cancelled by: {rental.cancelledBy}
|
||||
</small>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{rental.itemPrivateMessage && rental.itemReviewVisible && (
|
||||
<div className="alert alert-info mt-2 mb-2 p-2 small">
|
||||
<strong>
|
||||
@@ -259,8 +337,28 @@ const MyListings: React.FC = () => {
|
||||
<button
|
||||
className="btn btn-sm btn-success"
|
||||
onClick={() => handleAcceptRental(rental.id)}
|
||||
disabled={isProcessingPayment === rental.id}
|
||||
>
|
||||
Accept
|
||||
{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"
|
||||
@@ -268,10 +366,31 @@ const MyListings: React.FC = () => {
|
||||
>
|
||||
Reject
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-sm btn-outline-danger"
|
||||
onClick={() => handleCancelClick(rental)}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
{(rental.status === "active" ||
|
||||
rental.status === "confirmed") && (
|
||||
{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)}
|
||||
@@ -440,6 +559,19 @@ const MyListings: React.FC = () => {
|
||||
onSuccess={handleReviewRenterSuccess}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Cancellation Modal */}
|
||||
{rentalToCancel && (
|
||||
<RentalCancellationModal
|
||||
show={showCancelModal}
|
||||
onHide={() => {
|
||||
setShowCancelModal(false);
|
||||
setRentalToCancel(null);
|
||||
}}
|
||||
rental={rentalToCancel}
|
||||
onCancellationComplete={handleCancellationComplete}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useAuth } from "../contexts/AuthContext";
|
||||
import { rentalAPI } from "../services/api";
|
||||
import { Rental } from "../types";
|
||||
import ReviewItemModal from "../components/ReviewModal";
|
||||
import ConfirmationModal from "../components/ConfirmationModal";
|
||||
import RentalCancellationModal from "../components/RentalCancellationModal";
|
||||
|
||||
const MyRentals: React.FC = () => {
|
||||
// Helper function to format time
|
||||
@@ -28,8 +28,7 @@ const MyRentals: React.FC = () => {
|
||||
const [showReviewModal, setShowReviewModal] = useState(false);
|
||||
const [selectedRental, setSelectedRental] = useState<Rental | null>(null);
|
||||
const [showCancelModal, setShowCancelModal] = useState(false);
|
||||
const [rentalToCancel, setRentalToCancel] = useState<string | null>(null);
|
||||
const [cancelling, setCancelling] = useState(false);
|
||||
const [rentalToCancel, setRentalToCancel] = useState<Rental | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetchRentals();
|
||||
@@ -46,25 +45,20 @@ const MyRentals: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancelClick = (rentalId: string) => {
|
||||
setRentalToCancel(rentalId);
|
||||
const handleCancelClick = (rental: Rental) => {
|
||||
setRentalToCancel(rental);
|
||||
setShowCancelModal(true);
|
||||
};
|
||||
|
||||
const confirmCancelRental = async () => {
|
||||
if (!rentalToCancel) return;
|
||||
|
||||
setCancelling(true);
|
||||
try {
|
||||
await rentalAPI.updateRentalStatus(rentalToCancel, "cancelled");
|
||||
fetchRentals();
|
||||
setShowCancelModal(false);
|
||||
setRentalToCancel(null);
|
||||
} catch (err: any) {
|
||||
alert("Failed to cancel rental");
|
||||
} finally {
|
||||
setCancelling(false);
|
||||
}
|
||||
const handleCancellationComplete = (updatedRental: Rental) => {
|
||||
// Update the rental in the list
|
||||
setRentals(prev =>
|
||||
prev.map(rental =>
|
||||
rental.id === updatedRental.id ? updatedRental : rental
|
||||
)
|
||||
);
|
||||
setShowCancelModal(false);
|
||||
setRentalToCancel(null);
|
||||
};
|
||||
|
||||
const handleReviewClick = (rental: Rental) => {
|
||||
@@ -161,8 +155,12 @@ const MyRentals: React.FC = () => {
|
||||
: "bg-danger"
|
||||
}`}
|
||||
>
|
||||
{rental.status.charAt(0).toUpperCase() +
|
||||
rental.status.slice(1)}
|
||||
{rental.status === "pending"
|
||||
? "Awaiting Owner Approval"
|
||||
: rental.status === "confirmed"
|
||||
? "Confirmed & Paid"
|
||||
: rental.status.charAt(0).toUpperCase() + rental.status.slice(1)
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -187,6 +185,14 @@ const MyRentals: React.FC = () => {
|
||||
</p>
|
||||
)}
|
||||
|
||||
{rental.status === "pending" && (
|
||||
<div className="alert alert-info mt-2 mb-2 p-2 small">
|
||||
<i className="bi bi-clock me-2"></i>
|
||||
<strong>Awaiting Approval:</strong> Your payment method is saved.
|
||||
You'll only be charged if the owner approves your request.
|
||||
</div>
|
||||
)}
|
||||
|
||||
{rental.renterPrivateMessage &&
|
||||
rental.renterReviewVisible && (
|
||||
<div className="alert alert-info mt-2 mb-2 p-2 small">
|
||||
@@ -199,19 +205,39 @@ const MyRentals: React.FC = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{rental.status === "cancelled" &&
|
||||
rental.rejectionReason && (
|
||||
<div className="alert alert-warning mt-2 mb-1 p-2 small">
|
||||
<strong>Rejection reason:</strong>{" "}
|
||||
{rental.rejectionReason}
|
||||
</div>
|
||||
)}
|
||||
{rental.status === "cancelled" && (
|
||||
<>
|
||||
{rental.rejectionReason && (
|
||||
<div className="alert alert-warning mt-2 mb-1 p-2 small">
|
||||
<strong>Rejection reason:</strong>{" "}
|
||||
{rental.rejectionReason}
|
||||
</div>
|
||||
)}
|
||||
{rental.refundAmount !== undefined && (
|
||||
<div className="alert alert-info mt-2 mb-1 p-2 small">
|
||||
<strong>
|
||||
<i className="bi bi-arrow-return-left me-1"></i>
|
||||
Refund:
|
||||
</strong>{" "}
|
||||
${rental.refundAmount?.toFixed(2) || "0.00"}
|
||||
{rental.refundProcessedAt && (
|
||||
<small className="d-block text-muted mt-1">
|
||||
Processed: {new Date(rental.refundProcessedAt).toLocaleDateString()}
|
||||
</small>
|
||||
)}
|
||||
{rental.refundReason && (
|
||||
<small className="d-block mt-1">{rental.refundReason}</small>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="d-flex gap-2 mt-3">
|
||||
{rental.status === "pending" && (
|
||||
{(rental.status === "pending" || rental.status === "confirmed") && (
|
||||
<button
|
||||
className="btn btn-sm btn-danger"
|
||||
onClick={() => handleCancelClick(rental.id)}
|
||||
onClick={() => handleCancelClick(rental)}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
@@ -269,20 +295,17 @@ const MyRentals: React.FC = () => {
|
||||
/>
|
||||
)}
|
||||
|
||||
<ConfirmationModal
|
||||
show={showCancelModal}
|
||||
onClose={() => {
|
||||
setShowCancelModal(false);
|
||||
setRentalToCancel(null);
|
||||
}}
|
||||
onConfirm={confirmCancelRental}
|
||||
title="Cancel Rental"
|
||||
message="Are you sure you want to cancel this rental? This action cannot be undone."
|
||||
confirmText="Yes, Cancel Rental"
|
||||
cancelText="Keep Rental"
|
||||
confirmButtonClass="btn-danger"
|
||||
loading={cancelling}
|
||||
/>
|
||||
{rentalToCancel && (
|
||||
<RentalCancellationModal
|
||||
show={showCancelModal}
|
||||
onHide={() => {
|
||||
setShowCancelModal(false);
|
||||
setRentalToCancel(null);
|
||||
}}
|
||||
rental={rentalToCancel}
|
||||
onCancellationComplete={handleCancellationComplete}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import { useParams, useNavigate, useSearchParams } from "react-router-dom";
|
||||
import { Item } from "../types";
|
||||
import { useAuth } from "../contexts/AuthContext";
|
||||
import { itemAPI, rentalAPI } from "../services/api";
|
||||
import StripePaymentForm from "../components/StripePaymentForm";
|
||||
import EmbeddedStripeCheckout from "../components/EmbeddedStripeCheckout";
|
||||
|
||||
const RentItem: React.FC = () => {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
@@ -27,6 +27,7 @@ const RentItem: React.FC = () => {
|
||||
});
|
||||
|
||||
const [totalCost, setTotalCost] = useState(0);
|
||||
const [completed, setCompleted] = useState(false);
|
||||
|
||||
const convertToUTC = (dateString: string, timeString: string): string => {
|
||||
if (!dateString || !timeString) {
|
||||
@@ -117,8 +118,29 @@ const RentItem: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handlePaymentSuccess = () => {
|
||||
console.log("Stripe checkout session created successfully");
|
||||
const getRentalData = () => {
|
||||
try {
|
||||
const startDateTime = convertToUTC(
|
||||
manualSelection.startDate,
|
||||
manualSelection.startTime
|
||||
);
|
||||
const endDateTime = convertToUTC(
|
||||
manualSelection.endDate,
|
||||
manualSelection.endTime
|
||||
);
|
||||
|
||||
return {
|
||||
itemId: id,
|
||||
startDateTime,
|
||||
endDateTime,
|
||||
deliveryMethod: formData.deliveryMethod,
|
||||
deliveryAddress: formData.deliveryAddress,
|
||||
totalAmount: totalCost,
|
||||
};
|
||||
} catch (error: any) {
|
||||
setError(error.message);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const handleChange = (
|
||||
@@ -173,42 +195,69 @@ const RentItem: React.FC = () => {
|
||||
|
||||
<div className="row">
|
||||
<div className="col-md-8">
|
||||
<div className="card mb-4">
|
||||
<div className="card-body">
|
||||
<h5 className="card-title">Payment</h5>
|
||||
|
||||
<StripePaymentForm
|
||||
total={totalCost}
|
||||
itemName={item.name}
|
||||
rentalData={{
|
||||
itemId: item.id,
|
||||
startDateTime: convertToUTC(
|
||||
manualSelection.startDate,
|
||||
manualSelection.startTime
|
||||
),
|
||||
endDateTime: convertToUTC(
|
||||
manualSelection.endDate,
|
||||
manualSelection.endTime
|
||||
),
|
||||
totalAmount: totalCost,
|
||||
deliveryMethod: "pickup",
|
||||
}}
|
||||
onSuccess={handlePaymentSuccess}
|
||||
onError={(error) => setError(error)}
|
||||
disabled={
|
||||
!manualSelection.startDate || !manualSelection.endDate
|
||||
}
|
||||
/>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary mt-2"
|
||||
onClick={() => navigate(`/items/${id}`)}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
{completed ? (
|
||||
<div className="card mb-4">
|
||||
<div className="card-body text-center">
|
||||
<div className="alert alert-success">
|
||||
<i className="bi bi-check-circle-fill display-1 text-success mb-3"></i>
|
||||
<h3>Rental Request Sent!</h3>
|
||||
<p className="mb-3">
|
||||
Your rental request has been submitted to the owner.
|
||||
You'll only be charged if they approve your request.
|
||||
</p>
|
||||
<div className="d-grid gap-2 d-md-block">
|
||||
<button
|
||||
className="btn btn-primary me-2"
|
||||
onClick={() => navigate("/my-rentals")}
|
||||
>
|
||||
View My Rentals
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-outline-secondary"
|
||||
onClick={() => navigate("/")}
|
||||
>
|
||||
Continue Browsing
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="card mb-4">
|
||||
<div className="card-body">
|
||||
<h5 className="card-title">Complete Your Rental Request</h5>
|
||||
<p className="text-muted small mb-3">
|
||||
Add your payment method to complete your rental request.
|
||||
You'll only be charged if the owner approves your request.
|
||||
</p>
|
||||
|
||||
{!manualSelection.startDate || !manualSelection.endDate || !getRentalData() ? (
|
||||
<div className="alert alert-info">
|
||||
<i className="bi bi-info-circle me-2"></i>
|
||||
Please complete the rental dates and details above to proceed with payment setup.
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<EmbeddedStripeCheckout
|
||||
rentalData={getRentalData()}
|
||||
onSuccess={() => setCompleted(true)}
|
||||
onError={(error) => setError(error)}
|
||||
/>
|
||||
|
||||
<div className="text-center mt-3">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-outline-secondary"
|
||||
onClick={() => navigate(`/items/${id}`)}
|
||||
>
|
||||
Cancel Request
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="col-md-4">
|
||||
|
||||
Reference in New Issue
Block a user