Files
rentall-app/frontend/src/pages/ItemRequestDetail.tsx
2025-10-10 15:26:07 -04:00

363 lines
13 KiB
TypeScript

import React, { useState, useEffect } from 'react';
import { useParams, useNavigate, Link } from 'react-router-dom';
import { useAuth } from '../contexts/AuthContext';
import { itemRequestAPI } from '../services/api';
import { ItemRequest, ItemRequestResponse } from '../types';
import RequestResponseModal from '../components/RequestResponseModal';
import AuthButton from '../components/AuthButton';
const ItemRequestDetail: React.FC = () => {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const { user } = useAuth();
const [request, setRequest] = useState<ItemRequest | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [showResponseModal, setShowResponseModal] = useState(false);
useEffect(() => {
if (id) {
fetchRequest();
}
}, [id]);
const fetchRequest = async () => {
try {
setLoading(true);
const response = await itemRequestAPI.getItemRequest(id!);
setRequest(response.data);
} catch (err: any) {
setError(err.response?.data?.error || 'Failed to fetch request details');
} finally {
setLoading(false);
}
};
const handleResponseSubmitted = () => {
fetchRequest();
};
const getStatusColor = (status: string) => {
switch (status) {
case 'open':
return 'success';
case 'fulfilled':
return 'primary';
case 'closed':
return 'secondary';
default:
return 'secondary';
}
};
const getResponseStatusColor = (status: string) => {
switch (status) {
case 'pending':
return 'warning';
case 'accepted':
return 'success';
case 'declined':
return 'danger';
case 'expired':
return 'secondary';
default:
return 'secondary';
}
};
const formatDate = (dateString?: string) => {
if (!dateString) return 'Not specified';
const date = new Date(dateString);
return date.toLocaleDateString();
};
const getLocationString = () => {
if (!request) return '';
const parts = [];
if (request.city) parts.push(request.city);
if (request.state) parts.push(request.state);
return parts.join(', ') || 'Location not specified';
};
if (loading) {
return (
<div className="container mt-5">
<div className="text-center">
<div className="spinner-border" role="status">
<span className="visually-hidden">Loading...</span>
</div>
</div>
</div>
);
}
if (error || !request) {
return (
<div className="container mt-5">
<div className="alert alert-danger" role="alert">
{error || 'Request not found'}
</div>
<button className="btn btn-secondary" onClick={() => navigate(-1)}>
Go Back
</button>
</div>
);
}
const isOwner = user?.id === request.requesterId;
const canRespond = user && !isOwner && request.status === 'open';
return (
<div className="container mt-4">
<div className="row">
<div className="col-lg-8">
<div className="card">
<div className="card-body">
<div className="d-flex justify-content-between align-items-start mb-3">
<div>
<h1 className="card-title">{request.title}</h1>
<p className="text-muted mb-2">
Requested by {request.requester?.firstName || 'Unknown'} {request.requester?.lastName || ''}
</p>
</div>
<span className={`badge bg-${getStatusColor(request.status)} fs-6`}>
{request.status.charAt(0).toUpperCase() + request.status.slice(1)}
</span>
</div>
<div className="mb-4">
<h5>Description</h5>
<p className="card-text">{request.description}</p>
</div>
<div className="row mb-4">
<div className="col-md-6">
<h6><i className="bi bi-geo-alt me-2"></i>Location</h6>
<p className="text-muted">{getLocationString()}</p>
</div>
<div className="col-md-6">
<h6><i className="bi bi-calendar me-2"></i>Timeline</h6>
<p className="text-muted">
{request.isFlexibleDates ? (
'Flexible dates'
) : (
`${formatDate(request.preferredStartDate)} - ${formatDate(request.preferredEndDate)}`
)}
</p>
</div>
</div>
{(request.maxPricePerDay || request.maxPricePerHour) && (
<div className="mb-4">
<h6><i className="bi bi-currency-dollar me-2"></i>Budget</h6>
<div className="text-muted">
{request.maxPricePerDay && <div>Up to ${request.maxPricePerDay} per day</div>}
{request.maxPricePerHour && <div>Up to ${request.maxPricePerHour} per hour</div>}
</div>
</div>
)}
<div className="mb-4">
<small className="text-muted">
Created on {new Date(request.createdAt).toLocaleDateString()}
{request.responseCount || 0} response{request.responseCount !== 1 ? 's' : ''}
</small>
</div>
{canRespond && (
<div className="d-grid">
<button
className="btn btn-primary"
onClick={() => setShowResponseModal(true)}
>
<i className="bi bi-reply me-2"></i>
Respond to Request
</button>
</div>
)}
{isOwner && (
<div className="d-flex gap-2">
<Link to={`/my-requests`} className="btn btn-outline-primary">
<i className="bi bi-arrow-left me-2"></i>
Back to My Requests
</Link>
</div>
)}
</div>
</div>
{request.responses && request.responses.length > 0 && (
<div className="card mt-4">
<div className="card-body">
<h5 className="mb-4">Responses ({request.responses.length})</h5>
{request.responses.map((response: ItemRequestResponse) => (
<div key={response.id} className="border-bottom pb-4 mb-4 last:border-bottom-0 last:pb-0 last:mb-0">
<div className="d-flex justify-content-between align-items-start mb-3">
<div className="d-flex align-items-center">
<div className="me-3">
<div className="bg-light rounded-circle d-flex align-items-center justify-content-center" style={{ width: '40px', height: '40px' }}>
<i className="bi bi-person"></i>
</div>
</div>
<div>
<strong>{response.responder?.firstName || 'Unknown'} {response.responder?.lastName || ''}</strong>
<br />
<small className="text-muted">
{new Date(response.createdAt).toLocaleDateString()} at {new Date(response.createdAt).toLocaleTimeString()}
</small>
</div>
</div>
<span className={`badge bg-${getResponseStatusColor(response.status)}`}>
{response.status.charAt(0).toUpperCase() + response.status.slice(1)}
</span>
</div>
<div className="ms-5">
<p className="mb-3">{response.message}</p>
{(response.offerPricePerDay || response.offerPricePerHour) && (
<div className="mb-2">
<strong>Offered Price:</strong>
<div className="text-muted">
{response.offerPricePerDay && <div>${response.offerPricePerDay} per day</div>}
{response.offerPricePerHour && <div>${response.offerPricePerHour} per hour</div>}
</div>
</div>
)}
{(response.availableStartDate || response.availableEndDate) && (
<div className="mb-2">
<strong>Availability:</strong>
<div className="text-muted">
{formatDate(response.availableStartDate)} - {formatDate(response.availableEndDate)}
</div>
</div>
)}
{response.contactInfo && (
<div className="mb-2">
<strong>Contact:</strong>
<span className="text-muted ms-2">{response.contactInfo}</span>
</div>
)}
{response.existingItem && (
<div className="mb-2">
<strong>Related Item:</strong>
<Link to={`/items/${response.existingItem.id}`} className="ms-2 text-decoration-none">
{response.existingItem.name}
</Link>
</div>
)}
</div>
</div>
))}
</div>
</div>
)}
</div>
<div className="col-lg-4">
<div className="card">
<div className="card-body">
<h6>Request Summary</h6>
<div className="mb-3">
<strong>Status:</strong>
<span className={`badge bg-${getStatusColor(request.status)} ms-2`}>
{request.status.charAt(0).toUpperCase() + request.status.slice(1)}
</span>
</div>
<div className="mb-3">
<strong>Requested by:</strong>
<div className="mt-1">
<Link to={`/users/${request.requester?.id}`} className="text-decoration-none">
{request.requester?.firstName || 'Unknown'} {request.requester?.lastName || ''}
</Link>
</div>
</div>
{(request.maxPricePerDay || request.maxPricePerHour) && (
<div className="mb-3">
<strong>Budget Range:</strong>
<div className="text-muted mt-1">
{request.maxPricePerDay && <div> ${request.maxPricePerDay}/day</div>}
{request.maxPricePerHour && <div> ${request.maxPricePerHour}/hour</div>}
</div>
</div>
)}
<div className="mb-3">
<strong>Timeline:</strong>
<div className="text-muted mt-1">
{request.isFlexibleDates ? (
'Flexible dates'
) : (
<div>
<div>From: {formatDate(request.preferredStartDate)}</div>
<div>To: {formatDate(request.preferredEndDate)}</div>
</div>
)}
</div>
</div>
<div className="mb-3">
<strong>Location:</strong>
<div className="text-muted mt-1">{getLocationString()}</div>
</div>
<div className="mb-3">
<strong>Responses:</strong>
<div className="text-muted mt-1">{request.responseCount || 0} received</div>
</div>
<hr />
<div className="d-grid gap-2">
{canRespond ? (
<button
className="btn btn-success"
onClick={() => setShowResponseModal(true)}
>
<i className="bi bi-reply me-2"></i>
Respond to Request
</button>
) : user && !isOwner ? (
<div className="text-muted text-center">
<small>This request is {request.status}</small>
</div>
) : !user ? (
<div className="text-center">
<AuthButton mode="login" className="btn btn-outline-primary">
Log in to Respond
</AuthButton>
</div>
) : null}
<button
className="btn btn-outline-secondary"
onClick={() => navigate(-1)}
>
<i className="bi bi-arrow-left me-2"></i>
Go Back
</button>
</div>
</div>
</div>
</div>
</div>
<RequestResponseModal
show={showResponseModal}
onHide={() => setShowResponseModal(false)}
request={request}
onResponseSubmitted={handleResponseSubmitted}
/>
</div>
);
};
export default ItemRequestDetail;