Files
rentall-app/frontend/src/pages/Owning.tsx

719 lines
26 KiB
TypeScript

import React, { useState, useEffect } from "react";
import { Link, useNavigate } from "react-router-dom";
import { useAuth } from "../contexts/AuthContext";
import api from "../services/api";
import { Item, Rental } from "../types";
import { rentalAPI, conditionCheckAPI } from "../services/api";
import ReviewRenterModal from "../components/ReviewRenterModal";
import RentalCancellationModal from "../components/RentalCancellationModal";
import DeclineRentalModal from "../components/DeclineRentalModal";
import ConditionCheckModal from "../components/ConditionCheckModal";
import ReturnStatusModal from "../components/ReturnStatusModal";
const Owning: React.FC = () => {
// Helper function to format time
const formatTime = (timeString?: string) => {
if (!timeString || timeString.trim() === "") return "";
try {
const [hour, minute] = timeString.split(":");
const hourNum = parseInt(hour);
const hour12 = hourNum === 0 ? 12 : hourNum > 12 ? hourNum - 12 : hourNum;
const period = hourNum < 12 ? "AM" : "PM";
return `${hour12}:${minute} ${period}`;
} catch (error) {
return "";
}
};
// 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 navigate = useNavigate();
const [listings, setListings] = useState<Item[]>([]);
const [ownerRentals, setOwnerRentals] = useState<Rental[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");
// Owner rental management state
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 [showDeclineModal, setShowDeclineModal] = useState(false);
const [rentalToDecline, setRentalToDecline] = 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 [showReturnStatusModal, setShowReturnStatusModal] = useState(false);
const [rentalForReturn, setRentalForReturn] = useState<Rental | null>(null);
useEffect(() => {
fetchListings();
fetchOwnerRentals();
fetchAvailableChecks();
}, [user]);
const fetchListings = async () => {
if (!user) return;
try {
setLoading(true);
setError("");
const response = await api.get("/items");
// Filter items to only show ones owned by current user
const myItems = response.data.items.filter(
(item: Item) => item.ownerId === user.id
);
setListings(myItems);
} catch (err: any) {
console.error("Error fetching listings:", err);
if (err.response && err.response.status >= 500) {
setError("Failed to get your listings. Please try again later.");
}
} finally {
setLoading(false);
}
};
const handleDelete = async (itemId: string) => {
if (!window.confirm("Are you sure you want to delete this listing?"))
return;
try {
await api.delete(`/items/${itemId}`);
setListings(listings.filter((item) => item.id !== itemId));
} catch (err: any) {
alert("Failed to delete listing");
}
};
const toggleAvailability = async (item: Item) => {
try {
await api.put(`/items/${item.id}`, {
...item,
isAvailable: !item.isAvailable,
});
setListings(
listings.map((i) =>
i.id === item.id ? { ...i, isAvailable: !i.isAvailable } : i
)
);
} catch (err: any) {
alert("Failed to update availability");
}
};
const fetchOwnerRentals = async () => {
try {
const response = await rentalAPI.getListings();
setOwnerRentals(response.data);
} catch (err: any) {
console.error("Failed to fetch owner rentals:", err);
}
};
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([]);
}
};
// Owner functionality handlers
const handleAcceptRental = async (rentalId: string) => {
try {
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();
fetchAvailableChecks(); // Refresh available checks after rental confirmation
// 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"
}`
);
} else {
alert("Failed to accept rental request");
}
} finally {
setIsProcessingPayment("");
}
};
const handleDeclineClick = (rental: Rental) => {
setRentalToDecline(rental);
setShowDeclineModal(true);
};
const handleDeclineComplete = (updatedRental: Rental) => {
// Update the rental in the owner rentals list
setOwnerRentals((prev) =>
prev.map((rental) =>
rental.id === updatedRental.id ? updatedRental : rental
)
);
setShowDeclineModal(false);
setRentalToDecline(null);
};
const handleCompleteClick = (rental: Rental) => {
setRentalForReturn(rental);
setShowReturnStatusModal(true);
};
const handleReturnStatusMarked = async (updatedRental: Rental) => {
// Update the rental in the list
setOwnerRentals((prev) =>
prev.map((rental) =>
rental.id === updatedRental.id ? updatedRental : rental
)
);
// 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 = () => {
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);
};
const handleConditionCheck = (rental: Rental, checkType: string) => {
setConditionCheckData({ rental, checkType });
setShowConditionCheckModal(true);
};
const handleConditionCheckSuccess = () => {
fetchAvailableChecks();
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
);
};
// Filter owner rentals - exclude cancelled (shown in Rental History)
const allOwnerRentals = ownerRentals
.filter((r) => ["pending", "confirmed", "active"].includes(r.status))
.sort((a, b) => {
const statusOrder = { pending: 0, confirmed: 1, active: 2 };
return (
statusOrder[a.status as keyof typeof statusOrder] -
statusOrder[b.status as keyof typeof statusOrder]
);
});
if (loading) {
return (
<div className="container mt-4">
<div className="text-center">
<div className="spinner-border" role="status">
<span className="visually-hidden">Loading...</span>
</div>
</div>
</div>
);
}
return (
<div className="container mt-4">
<h1 className="mb-4">Owning</h1>
{error && (
<div className="alert alert-danger" role="alert">
{error}
</div>
)}
{/* Rental Requests Section - Moved to top for priority */}
{allOwnerRentals.length > 0 && (
<div className="mb-5">
<h4 className="mb-3">
<i className="bi bi-calendar-check me-2"></i>
Rentals
</h4>
<div className="row">
{allOwnerRentals.map((rental) => (
<div key={rental.id} className="col-md-6 col-lg-4 mb-4">
<div className="card h-100">
{rental.item?.images && rental.item.images[0] && (
<img
src={rental.item.images[0]}
className="card-img-top"
alt={rental.item.name}
style={{ height: "200px", objectFit: "cover" }}
/>
)}
<div className="card-body">
<h5 className="card-title text-dark">
{rental.item ? rental.item.name : "Item Unavailable"}
</h5>
{rental.renter && (
<p className="mb-1 text-dark small">
<strong>Renter:</strong>{" "}
<span
onClick={() => navigate(`/users/${rental.renterId}`)}
style={{ cursor: "pointer" }}
>
{rental.renter.firstName} {rental.renter.lastName}
</span>
</p>
)}
<div className="mb-2">
<span
className={`badge ${
rental.status === "active"
? "bg-success"
: rental.status === "pending"
? "bg-warning"
: rental.status === "confirmed"
? "bg-info"
: "bg-danger"
}`}
>
{rental.status.charAt(0).toUpperCase() +
rental.status.slice(1)}
</span>
</div>
<p className="mb-1 text-dark small">
<strong>Period:</strong>
<br />
{formatDateTime(rental.startDateTime)} -{" "}
{formatDateTime(rental.endDateTime)}
</p>
<p className="mb-1 text-dark small">
<strong>Total:</strong> ${rental.totalAmount}
</p>
{rental.intendedUse && rental.status === "pending" && (
<div className="alert alert-light mt-2 mb-2 p-2 small">
<strong>Intended Use:</strong>
<br />
{rental.intendedUse}
</div>
)}
{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>
<i className="bi bi-envelope-fill me-1"></i>Private
Note from Renter:
</strong>
<br />
{rental.itemPrivateMessage}
</div>
)}
<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>
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)}
>
Decline
</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>
)}
</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 === "pre_rental_owner"
? "Submit Pre-Rental Check"
: "Submit Post-Rental Check"}
</button>
))}
</div>
</div>
</div>
</div>
))}
</div>
</div>
)}
<div className="d-flex justify-content-between align-items-center mb-3">
<h4 className="mb-0">
<i className="bi bi-list-ul me-2"></i>
Listings
</h4>
<Link to="/create-item" className="btn btn-primary">
Add New Item
</Link>
</div>
{listings.length === 0 ? (
<div className="text-center py-5">
<p className="text-muted">You haven't listed any items yet.</p>
<Link to="/create-item" className="btn btn-primary mt-3">
List Your First Item
</Link>
</div>
) : (
<div className="row">
{listings.map((item) => (
<div key={item.id} className="col-md-6 col-lg-4 mb-4">
<div
className="card h-100"
style={{ cursor: "pointer" }}
onClick={(e: React.MouseEvent<HTMLDivElement>) => {
const target = e.target as HTMLElement;
if (target.closest("button") || target.closest("a")) {
return;
}
navigate(`/items/${item.id}`);
}}
>
{item.images && item.images[0] && (
<img
src={item.images[0]}
className="card-img-top"
alt={item.name}
style={{ height: "200px", objectFit: "cover" }}
/>
)}
<div className="card-body">
<h5 className="card-title text-dark">{item.name}</h5>
<p className="card-text text-truncate text-dark">
{item.description}
</p>
<div className="mb-2 d-flex gap-2 flex-wrap">
<span
className={`badge ${
item.isAvailable ? "bg-success" : "bg-secondary"
}`}
>
{item.isAvailable ? "Available" : "Not Available"}
</span>
{item.isDeleted && (
<span className="badge bg-danger">
<i className="bi bi-exclamation-triangle-fill me-1"></i>
Deleted by Admin
</span>
)}
</div>
<div className="mb-3">
{(() => {
const hasAnyPositivePrice =
(item.pricePerHour !== undefined &&
Number(item.pricePerHour) > 0) ||
(item.pricePerDay !== undefined &&
Number(item.pricePerDay) > 0) ||
(item.pricePerWeek !== undefined &&
Number(item.pricePerWeek) > 0) ||
(item.pricePerMonth !== undefined &&
Number(item.pricePerMonth) > 0);
const hasAnyZeroPrice =
(item.pricePerHour !== undefined &&
Number(item.pricePerHour) === 0) ||
(item.pricePerDay !== undefined &&
Number(item.pricePerDay) === 0) ||
(item.pricePerWeek !== undefined &&
Number(item.pricePerWeek) === 0) ||
(item.pricePerMonth !== undefined &&
Number(item.pricePerMonth) === 0);
if (!hasAnyPositivePrice && hasAnyZeroPrice) {
return (
<div className="text-success small fw-bold">
Free to Borrow
</div>
);
}
return (
<>
{item.pricePerDay && Number(item.pricePerDay) > 0 && (
<div className="text-muted small">
${item.pricePerDay}/day
</div>
)}
{item.pricePerHour &&
Number(item.pricePerHour) > 0 && (
<div className="text-muted small">
${item.pricePerHour}/hour
</div>
)}
{item.pricePerWeek &&
Number(item.pricePerWeek) > 0 && (
<div className="text-muted small">
${item.pricePerWeek}/week
</div>
)}
{item.pricePerMonth &&
Number(item.pricePerMonth) > 0 && (
<div className="text-muted small">
${item.pricePerMonth}/month
</div>
)}
</>
);
})()}
</div>
<div className="d-flex gap-2">
<Link
to={`/items/${item.id}/edit`}
className="btn btn-sm btn-outline-primary"
>
Edit
</Link>
<button
onClick={() => toggleAvailability(item)}
className="btn btn-sm btn-outline-info"
>
{item.isAvailable ? "Mark Unavailable" : "Mark Available"}
</button>
<button
onClick={() => handleDelete(item.id)}
className="btn btn-sm btn-outline-danger"
>
Delete
</button>
</div>
</div>
</div>
</div>
))}
</div>
)}
{/* Review Modal */}
{selectedRentalForReview && (
<ReviewRenterModal
show={showReviewRenterModal}
onClose={() => {
setShowReviewRenterModal(false);
setSelectedRentalForReview(null);
}}
rental={selectedRentalForReview}
onSuccess={handleReviewRenterSuccess}
/>
)}
{/* Cancellation Modal */}
{rentalToCancel && (
<RentalCancellationModal
show={showCancelModal}
onHide={() => {
setShowCancelModal(false);
setRentalToCancel(null);
}}
rental={rentalToCancel}
onCancellationComplete={handleCancellationComplete}
/>
)}
{/* Decline Modal */}
{rentalToDecline && (
<DeclineRentalModal
show={showDeclineModal}
onHide={() => {
setShowDeclineModal(false);
setRentalToDecline(null);
}}
rental={rentalToDecline}
onDeclineComplete={handleDeclineComplete}
/>
)}
{/* 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>
);
};
export default Owning;