From 5d85f77a1903f5b9050f28ecbedd8bb60fb9714b Mon Sep 17 00:00:00 2001 From: jackiettran <41605212+jackiettran@users.noreply.github.com> Date: Fri, 22 Aug 2025 20:18:22 -0400 Subject: [PATCH] restructuring for rental requests. started double blind reviews --- backend/models/Item.js | 4 - backend/models/Rental.js | 50 + backend/routes/rentals.js | 171 ++- frontend/src/components/AuthModal.tsx | 2 +- .../src/components/AvailabilityCalendar.tsx | 1000 ----------------- frontend/src/components/Navbar.tsx | 6 +- frontend/src/components/ReviewModal.tsx | 46 +- frontend/src/components/ReviewRenterModal.tsx | 176 +++ frontend/src/pages/ItemDetail.tsx | 109 +- frontend/src/pages/MyListings.tsx | 499 ++++---- frontend/src/pages/MyRentals.tsx | 283 +++-- frontend/src/pages/Profile.tsx | 334 +++++- frontend/src/pages/RentItem.tsx | 2 +- frontend/src/services/api.ts | 5 +- frontend/src/types/index.ts | 21 +- 15 files changed, 1305 insertions(+), 1403 deletions(-) delete mode 100644 frontend/src/components/AvailabilityCalendar.tsx create mode 100644 frontend/src/components/ReviewRenterModal.tsx diff --git a/backend/models/Item.js b/backend/models/Item.js index 4e423a9..103dc7f 100644 --- a/backend/models/Item.js +++ b/backend/models/Item.js @@ -103,10 +103,6 @@ const Item = sequelize.define("Item", { allowNull: false, defaultValue: false, }, - unavailablePeriods: { - type: DataTypes.JSONB, - defaultValue: [], - }, availableAfter: { type: DataTypes.STRING, defaultValue: "09:00", diff --git a/backend/models/Rental.js b/backend/models/Rental.js index befd029..daee2c4 100644 --- a/backend/models/Rental.js +++ b/backend/models/Rental.js @@ -39,6 +39,12 @@ const Rental = sequelize.define('Rental', { type: DataTypes.DATE, allowNull: false }, + startTime: { + type: DataTypes.STRING + }, + endTime: { + type: DataTypes.STRING + }, totalAmount: { type: DataTypes.DECIMAL(10, 2), allowNull: false @@ -61,6 +67,50 @@ const Rental = sequelize.define('Rental', { notes: { type: DataTypes.TEXT }, + // Renter's review of the item (existing fields renamed for clarity) + itemRating: { + type: DataTypes.INTEGER, + validate: { + min: 1, + max: 5 + } + }, + itemReview: { + type: DataTypes.TEXT + }, + itemReviewSubmittedAt: { + type: DataTypes.DATE + }, + itemReviewVisible: { + type: DataTypes.BOOLEAN, + defaultValue: false + }, + // Owner's review of the renter + renterRating: { + type: DataTypes.INTEGER, + validate: { + min: 1, + max: 5 + } + }, + renterReview: { + type: DataTypes.TEXT + }, + renterReviewSubmittedAt: { + type: DataTypes.DATE + }, + renterReviewVisible: { + type: DataTypes.BOOLEAN, + defaultValue: false + }, + // Private messages (always visible to recipient) + itemPrivateMessage: { + type: DataTypes.TEXT + }, + renterPrivateMessage: { + type: DataTypes.TEXT + }, + // Legacy fields for backwards compatibility rating: { type: DataTypes.INTEGER, validate: { diff --git a/backend/routes/rentals.js b/backend/routes/rentals.js index 4449a4b..2b3c8a7 100644 --- a/backend/routes/rentals.js +++ b/backend/routes/rentals.js @@ -4,10 +4,53 @@ const { Rental, Item, User } = require('../models'); // Import from models/index const { authenticateToken } = require('../middleware/auth'); const router = express.Router(); +// Helper function to check and update review visibility +const checkAndUpdateReviewVisibility = async (rental) => { + const now = new Date(); + const tenMinutesInMs = 10 * 60 * 1000; // 10 minutes + + let needsUpdate = false; + let updates = {}; + + // Check if both reviews are submitted + if (rental.itemReviewSubmittedAt && rental.renterReviewSubmittedAt) { + if (!rental.itemReviewVisible || !rental.renterReviewVisible) { + updates.itemReviewVisible = true; + updates.renterReviewVisible = true; + needsUpdate = true; + } + } else { + // Check item review visibility (10-minute rule) + if (rental.itemReviewSubmittedAt && !rental.itemReviewVisible) { + const timeSinceSubmission = now - new Date(rental.itemReviewSubmittedAt); + if (timeSinceSubmission >= tenMinutesInMs) { + updates.itemReviewVisible = true; + needsUpdate = true; + } + } + + // Check renter review visibility (10-minute rule) + if (rental.renterReviewSubmittedAt && !rental.renterReviewVisible) { + const timeSinceSubmission = now - new Date(rental.renterReviewSubmittedAt); + if (timeSinceSubmission >= tenMinutesInMs) { + updates.renterReviewVisible = true; + needsUpdate = true; + } + } + } + + if (needsUpdate) { + await rental.update(updates); + } + + return rental; +}; + router.get('/my-rentals', authenticateToken, async (req, res) => { try { const rentals = await Rental.findAll({ where: { renterId: req.user.id }, + // Remove explicit attributes to let Sequelize handle missing columns gracefully include: [ { model: Item, as: 'item' }, { model: User, as: 'owner', attributes: ['id', 'username', 'firstName', 'lastName'] } @@ -15,8 +58,10 @@ router.get('/my-rentals', authenticateToken, async (req, res) => { order: [['createdAt', 'DESC']] }); + console.log('My-rentals data:', rentals.length > 0 ? rentals[0].toJSON() : 'No rentals'); res.json(rentals); } catch (error) { + console.error('Error in my-rentals route:', error); res.status(500).json({ error: error.message }); } }); @@ -25,6 +70,7 @@ router.get('/my-listings', authenticateToken, async (req, res) => { try { const rentals = await Rental.findAll({ where: { ownerId: req.user.id }, + // Remove explicit attributes to let Sequelize handle missing columns gracefully include: [ { model: Item, as: 'item' }, { model: User, as: 'renter', attributes: ['id', 'username', 'firstName', 'lastName'] } @@ -32,15 +78,17 @@ router.get('/my-listings', authenticateToken, async (req, res) => { order: [['createdAt', 'DESC']] }); + console.log('My-listings rentals:', rentals.length > 0 ? rentals[0].toJSON() : 'No rentals'); res.json(rentals); } catch (error) { + console.error('Error in my-listings route:', error); res.status(500).json({ error: error.message }); } }); router.post('/', authenticateToken, async (req, res) => { try { - const { itemId, startDate, endDate, deliveryMethod, deliveryAddress, notes } = req.body; + const { itemId, startDate, endDate, startTime, endTime, deliveryMethod, deliveryAddress, notes } = req.body; const item = await Item.findByPk(itemId); if (!item) { @@ -79,6 +127,8 @@ router.post('/', authenticateToken, async (req, res) => { ownerId: item.ownerId, startDate, endDate, + startTime, + endTime, totalAmount, deliveryMethod, deliveryAddress, @@ -128,6 +178,125 @@ router.put('/:id/status', authenticateToken, async (req, res) => { } }); +// Owner reviews renter +router.post('/:id/review-renter', authenticateToken, async (req, res) => { + try { + const { rating, review, privateMessage } = req.body; + const rental = await Rental.findByPk(req.params.id); + + if (!rental) { + return res.status(404).json({ error: 'Rental not found' }); + } + + if (rental.ownerId !== req.user.id) { + return res.status(403).json({ error: 'Only owners can review renters' }); + } + + if (rental.status !== 'completed') { + return res.status(400).json({ error: 'Can only review completed rentals' }); + } + + if (rental.renterReviewSubmittedAt) { + return res.status(400).json({ error: 'Renter review already submitted' }); + } + + // Submit the review and private message + await rental.update({ + renterRating: rating, + renterReview: review, + renterReviewSubmittedAt: new Date(), + renterPrivateMessage: privateMessage + }); + + // Check and update visibility + const updatedRental = await checkAndUpdateReviewVisibility(rental); + + res.json({ + success: true, + reviewVisible: updatedRental.renterReviewVisible + }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// Renter reviews item +router.post('/:id/review-item', authenticateToken, async (req, res) => { + try { + const { rating, review, privateMessage } = req.body; + const rental = await Rental.findByPk(req.params.id); + + if (!rental) { + return res.status(404).json({ error: 'Rental not found' }); + } + + if (rental.renterId !== req.user.id) { + return res.status(403).json({ error: 'Only renters can review items' }); + } + + if (rental.status !== 'completed') { + return res.status(400).json({ error: 'Can only review completed rentals' }); + } + + if (rental.itemReviewSubmittedAt) { + return res.status(400).json({ error: 'Item review already submitted' }); + } + + // Submit the review and private message + await rental.update({ + itemRating: rating, + itemReview: review, + itemReviewSubmittedAt: new Date(), + itemPrivateMessage: privateMessage + }); + + // Check and update visibility + const updatedRental = await checkAndUpdateReviewVisibility(rental); + + res.json({ + success: true, + reviewVisible: updatedRental.itemReviewVisible + }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// Mark rental as completed (owner only) +router.post('/:id/mark-completed', authenticateToken, async (req, res) => { + try { + console.log('Mark completed endpoint hit for rental ID:', req.params.id); + const rental = await Rental.findByPk(req.params.id); + + if (!rental) { + return res.status(404).json({ error: 'Rental not found' }); + } + + if (rental.ownerId !== req.user.id) { + return res.status(403).json({ error: 'Only owners can mark rentals as completed' }); + } + + if (!['active', 'confirmed'].includes(rental.status)) { + return res.status(400).json({ error: 'Can only mark active or confirmed rentals as completed' }); + } + + await rental.update({ status: 'completed' }); + + const updatedRental = await Rental.findByPk(rental.id, { + include: [ + { model: Item, as: 'item' }, + { model: User, as: 'owner', attributes: ['id', 'username', 'firstName', 'lastName'] }, + { model: User, as: 'renter', attributes: ['id', 'username', 'firstName', 'lastName'] } + ] + }); + + res.json(updatedRental); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// Legacy review endpoint (for backward compatibility) router.post('/:id/review', authenticateToken, async (req, res) => { try { const { rating, review } = req.body; diff --git a/frontend/src/components/AuthModal.tsx b/frontend/src/components/AuthModal.tsx index 4624a65..31c1a9b 100644 --- a/frontend/src/components/AuthModal.tsx +++ b/frontend/src/components/AuthModal.tsx @@ -310,7 +310,7 @@ const AuthModal: React.FC = ({ void; - mode: 'owner' | 'renter'; // Clear mode distinction - onRentalSelect?: (period: { startDate: Date; endDate: Date; startTime?: string; endTime?: string }) => void; - selectedRentalPeriod?: UnavailablePeriod; // External selection to display -} - -type ViewType = 'month' | 'week' | 'day'; - -const AvailabilityCalendar: React.FC = ({ - unavailablePeriods, - onPeriodsChange, - mode, - onRentalSelect, - selectedRentalPeriod: externalSelectedPeriod -}) => { - const [currentDate, setCurrentDate] = useState(new Date()); - const [viewType, setViewType] = useState('month'); - const [selectionStart, setSelectionStart] = useState(null); - const [selectionStartHour, setSelectionStartHour] = useState(null); - const [internalSelectedPeriod, setInternalSelectedPeriod] = useState(null); - const [ownerSelectionStart, setOwnerSelectionStart] = useState(null); - const [ownerSelectionStartHour, setOwnerSelectionStartHour] = useState(null); - - // Use external selection if provided, otherwise use internal - const selectedRentalPeriod = externalSelectedPeriod || internalSelectedPeriod; - - const getDaysInMonth = (date: Date) => { - const year = date.getFullYear(); - const month = date.getMonth(); - const firstDay = new Date(year, month, 1); - const lastDay = new Date(year, month + 1, 0); - const daysInMonth = lastDay.getDate(); - const startingDayOfWeek = firstDay.getDay(); - - const days: (Date | null)[] = []; - - // Add empty cells for days before month starts - for (let i = 0; i < startingDayOfWeek; i++) { - days.push(null); - } - - // Add all days in month - for (let i = 1; i <= daysInMonth; i++) { - days.push(new Date(year, month, i)); - } - - return days; - }; - - const getWeekDays = (date: Date) => { - const startOfWeek = new Date(date); - const day = startOfWeek.getDay(); - const diff = startOfWeek.getDate() - day; - startOfWeek.setDate(diff); - - const days: Date[] = []; - for (let i = 0; i < 7; i++) { - const day = new Date(startOfWeek); - day.setDate(startOfWeek.getDate() + i); - days.push(day); - } - return days; - }; - - const isDateInPeriod = (date: Date, period: UnavailablePeriod) => { - const start = new Date(period.startDate); - const end = new Date(period.endDate); - const checkDate = new Date(date); - - // Normalize all dates to midnight for comparison - start.setHours(0, 0, 0, 0); - end.setHours(0, 0, 0, 0); - checkDate.setHours(0, 0, 0, 0); - - // Basic date range check - return checkDate >= start && checkDate <= end; - }; - - const isDateUnavailable = (date: Date) => { - return unavailablePeriods.some(period => { - const start = new Date(period.startDate); - const end = new Date(period.endDate); - start.setHours(0, 0, 0, 0); - end.setHours(23, 59, 59, 999); - const checkDate = new Date(date); - checkDate.setHours(0, 0, 0, 0); - return checkDate >= start && checkDate <= end; - }); - }; - - const isDateFullyUnavailable = (date: Date) => { - // Check if there's a period that covers the entire day without specific times - const hasFullDayPeriod = unavailablePeriods.some(period => { - const start = new Date(period.startDate); - const end = new Date(period.endDate); - const checkDate = new Date(date); - - start.setHours(0, 0, 0, 0); - end.setHours(0, 0, 0, 0); - checkDate.setHours(0, 0, 0, 0); - - return checkDate >= start && checkDate <= end && !period.startTime && !period.endTime; - }); - - if (hasFullDayPeriod) return true; - - // Check if all 24 hours are covered by hour-specific periods - const dateStr = date.toISOString().split('T')[0]; - const hoursWithPeriods = new Set(); - - unavailablePeriods.forEach(period => { - const periodDateStr = new Date(period.startDate).toISOString().split('T')[0]; - if (periodDateStr === dateStr && period.startTime && period.endTime) { - const startHour = parseInt(period.startTime.split(':')[0]); - const endHour = parseInt(period.endTime.split(':')[0]); - for (let h = startHour; h <= endHour; h++) { - hoursWithPeriods.add(h); - } - } - }); - - return hoursWithPeriods.size === 24; - }; - - const isDatePartiallyUnavailable = (date: Date) => { - return isDateUnavailable(date) && !isDateFullyUnavailable(date); - }; - - const isHourUnavailable = (date: Date, hour: number) => { - return unavailablePeriods.some(period => { - const start = new Date(period.startDate); - const end = new Date(period.endDate); - - // Check if date is within period - const dateOnly = new Date(date); - dateOnly.setHours(0, 0, 0, 0); - start.setHours(0, 0, 0, 0); - end.setHours(0, 0, 0, 0); - - if (dateOnly < start || dateOnly > end) return false; - - // If no specific times, entire day is unavailable - if (!period.startTime || !period.endTime) return true; - - // Check specific hour - const startHour = parseInt(period.startTime.split(':')[0]); - const endHour = parseInt(period.endTime.split(':')[0]); - - return hour >= startHour && hour <= endHour; - }); - }; - - const handleHourClick = (date: Date, hour: number) => { - if (mode !== 'renter') return; - - // Check if this hour is unavailable - if (isHourUnavailable(date, hour)) return; - - if (!selectionStart || !selectionStartHour) { - // First click - set start date and hour - setSelectionStart(date); - setSelectionStartHour(hour); - setInternalSelectedPeriod(null); - } else { - // Second click - complete selection - const startDate = new Date(selectionStart); - const endDate = new Date(date); - - // Determine which date/hour comes first - let finalStart, finalEnd, startHour, endHour; - - if (startDate.toDateString() === endDate.toDateString()) { - // Same day - compare hours - if (selectionStartHour <= hour) { - finalStart = startDate; - finalEnd = endDate; - startHour = selectionStartHour; - endHour = hour; - } else { - finalStart = endDate; - finalEnd = startDate; - startHour = hour; - endHour = selectionStartHour; - } - } else if (startDate < endDate) { - finalStart = startDate; - finalEnd = endDate; - startHour = selectionStartHour; - endHour = hour; - } else { - finalStart = endDate; - finalEnd = startDate; - startHour = hour; - endHour = selectionStartHour; - } - - // Set the hours - finalStart.setHours(startHour, 0, 0, 0); - finalEnd.setHours(endHour, 59, 59, 999); - - // Check if any hour in range is unavailable - let current = new Date(finalStart); - let hasUnavailable = false; - - while (current <= finalEnd) { - const currentHour = current.getHours(); - if (isHourUnavailable(current, currentHour)) { - hasUnavailable = true; - break; - } - current.setHours(current.getHours() + 1); - } - - if (hasUnavailable) { - // Range contains unavailable hours, reset - setSelectionStart(null); - setSelectionStartHour(null); - return; - } - - // Create rental selection - const rentalPeriod: UnavailablePeriod = { - id: 'rental-selection', - startDate: finalStart, - endDate: finalEnd, - startTime: `${startHour.toString().padStart(2, '0')}:00`, - endTime: `${endHour.toString().padStart(2, '0')}:00`, - isRentalSelection: true - }; - - setInternalSelectedPeriod(rentalPeriod); - setSelectionStart(null); - setSelectionStartHour(null); - - // Notify parent - if (onRentalSelect) { - onRentalSelect({ - startDate: finalStart, - endDate: finalEnd, - startTime: `${startHour.toString().padStart(2, '0')}:00`, - endTime: `${endHour.toString().padStart(2, '0')}:00` - }); - } - } - }; - - const handleDateClick = (date: Date) => { - if (mode === 'owner') { - // OWNER MODE: Toggle unavailability for dates - // Check if this date has an accepted rental - can't modify those - const hasAcceptedRental = unavailablePeriods.some(p => - p.isAcceptedRental && isDateInPeriod(date, p) - ); - - if (hasAcceptedRental) { - return; // Can't modify dates with accepted rentals - } - - // Check if this date is part of any unavailable period - const isUnavailable = isDateUnavailable(date); - - if (isUnavailable && !ownerSelectionStart) { - // Single click on an unavailable date - split periods to make just this date available - const updatedPeriods: UnavailablePeriod[] = []; - - unavailablePeriods.forEach(period => { - if (period.isAcceptedRental || !isDateInPeriod(date, period)) { - // Keep accepted rentals and periods that don't contain this date - updatedPeriods.push(period); - } else { - // Split the period around this date - const periodStart = new Date(period.startDate); - const periodEnd = new Date(period.endDate); - const targetDate = new Date(date); - - periodStart.setHours(0, 0, 0, 0); - periodEnd.setHours(0, 0, 0, 0); - targetDate.setHours(0, 0, 0, 0); - - // If there are dates before the target date, create a period for them - if (periodStart < targetDate) { - const beforeDate = new Date(targetDate); - beforeDate.setDate(beforeDate.getDate() - 1); - updatedPeriods.push({ - id: Date.now().toString() + '-before', - startDate: periodStart, - endDate: beforeDate, - startTime: period.startTime, - endTime: period.endTime - }); - } - - // If there are dates after the target date, create a period for them - if (periodEnd > targetDate) { - const afterDate = new Date(targetDate); - afterDate.setDate(afterDate.getDate() + 1); - updatedPeriods.push({ - id: Date.now().toString() + '-after', - startDate: afterDate, - endDate: periodEnd, - startTime: period.startTime, - endTime: period.endTime - }); - } - } - }); - - onPeriodsChange(updatedPeriods); - setOwnerSelectionStart(null); - } else if (!ownerSelectionStart) { - // First click - start selection - setOwnerSelectionStart(date); - } else { - // Second click - complete range selection - const start = new Date(Math.min(ownerSelectionStart.getTime(), date.getTime())); - const end = new Date(Math.max(ownerSelectionStart.getTime(), date.getTime())); - - // Check if any date in range has accepted rental - let current = new Date(start); - let hasAcceptedInRange = false; - while (current <= end) { - if (unavailablePeriods.some(p => p.isAcceptedRental && isDateInPeriod(current, p))) { - hasAcceptedInRange = true; - break; - } - current.setDate(current.getDate() + 1); - } - - if (hasAcceptedInRange) { - // Can't select range with accepted rentals - setOwnerSelectionStart(null); - return; - } - - // Add new unavailable period for the range - const newPeriod: UnavailablePeriod = { - id: Date.now().toString(), - startDate: start, - endDate: end - }; - onPeriodsChange([...unavailablePeriods, newPeriod]); - setOwnerSelectionStart(null); - } - } else { - // RENTER MODE: Select rental period - // Check if date is unavailable - if (isDateUnavailable(date)) { - return; // Can't select unavailable dates - } - - if (!selectionStart) { - // First click - set start date - setSelectionStart(date); - setSelectionStartHour(null); // Reset hour selection - setInternalSelectedPeriod(null); - } else { - // Second click - complete selection - const start = new Date(Math.min(selectionStart.getTime(), date.getTime())); - const end = new Date(Math.max(selectionStart.getTime(), date.getTime())); - - // Check if any date in range is unavailable - let current = new Date(start); - let hasUnavailable = false; - while (current <= end) { - if (isDateUnavailable(current)) { - hasUnavailable = true; - break; - } - current.setDate(current.getDate() + 1); - } - - if (hasUnavailable) { - // Range contains unavailable dates, reset - setSelectionStart(null); - return; - } - - // Create rental selection - const rentalPeriod: UnavailablePeriod = { - id: 'rental-selection', - startDate: start, - endDate: end, - isRentalSelection: true - }; - - setInternalSelectedPeriod(rentalPeriod); - setSelectionStart(null); - setSelectionStartHour(null); - - // Notify parent if callback provided - if (onRentalSelect) { - onRentalSelect({ - startDate: start, - endDate: end - }); - } - } - } - }; - - - const toggleHourAvailability = (date: Date, hour: number) => { - if (mode !== 'owner') return; // Only owners can toggle hour availability - - // Check if this hour has an accepted rental - const hasAcceptedRental = unavailablePeriods.some(p => - p.isAcceptedRental && isHourUnavailable(date, hour) - ); - - if (hasAcceptedRental) { - return; // Can't modify hours with accepted rentals - } - - const isUnavailable = isHourUnavailable(date, hour); - - if (isUnavailable && !ownerSelectionStartHour) { - // Make just this hour available by splitting periods - const updatedPeriods: UnavailablePeriod[] = []; - - unavailablePeriods.forEach(period => { - if (period.isAcceptedRental || !isDateInPeriod(date, period)) { - // Keep accepted rentals and periods that don't contain this date - updatedPeriods.push(period); - } else if (!period.startTime || !period.endTime) { - // This is a full-day period, split it into hourly periods excluding this hour - for (let h = 0; h < 24; h++) { - if (h !== hour) { - updatedPeriods.push({ - id: `${period.id}-hour-${h}`, - startDate: date, - endDate: date, - startTime: `${h.toString().padStart(2, '0')}:00`, - endTime: `${h.toString().padStart(2, '0')}:59` - }); - } - } - } else { - // This is an hourly period, check if it contains this hour - const startHour = parseInt(period.startTime.split(':')[0]); - const endHour = parseInt(period.endTime.split(':')[0]); - - if (hour < startHour || hour > endHour) { - // Hour is outside this period, keep the period - updatedPeriods.push(period); - } else { - // Split the period around this hour - if (startHour < hour) { - updatedPeriods.push({ - id: period.id + '-before', - startDate: period.startDate, - endDate: period.endDate, - startTime: period.startTime, - endTime: `${(hour - 1).toString().padStart(2, '0')}:59` - }); - } - - if (endHour > hour) { - updatedPeriods.push({ - id: period.id + '-after', - startDate: period.startDate, - endDate: period.endDate, - startTime: `${(hour + 1).toString().padStart(2, '0')}:00`, - endTime: period.endTime - }); - } - } - } - }); - - onPeriodsChange(updatedPeriods); - setOwnerSelectionStart(null); - setOwnerSelectionStartHour(null); - } else if (!ownerSelectionStartHour) { - // First click - start selection - setOwnerSelectionStart(date); - setOwnerSelectionStartHour(hour); - } else { - // Second click - complete selection - const startDate = new Date(ownerSelectionStart!); - const endDate = new Date(date); - - // Determine which date/hour comes first - let finalStart, finalEnd, startHour, endHour; - - if (startDate.toDateString() === endDate.toDateString()) { - // Same day - compare hours - if (ownerSelectionStartHour <= hour) { - finalStart = startDate; - finalEnd = endDate; - startHour = ownerSelectionStartHour; - endHour = hour; - } else { - finalStart = endDate; - finalEnd = startDate; - startHour = hour; - endHour = ownerSelectionStartHour; - } - } else if (startDate < endDate) { - finalStart = startDate; - finalEnd = endDate; - startHour = ownerSelectionStartHour; - endHour = hour; - } else { - finalStart = endDate; - finalEnd = startDate; - startHour = hour; - endHour = ownerSelectionStartHour; - } - - // Check if any hour in range has accepted rental - let current = new Date(finalStart); - current.setHours(startHour, 0, 0, 0); - const endDateTime = new Date(finalEnd); - endDateTime.setHours(endHour, 59, 59, 999); - let hasAcceptedInRange = false; - - while (current <= endDateTime) { - const currentHour = current.getHours(); - if (unavailablePeriods.some(p => - p.isAcceptedRental && isHourUnavailable(current, currentHour) - )) { - hasAcceptedInRange = true; - break; - } - current.setHours(current.getHours() + 1); - } - - if (hasAcceptedInRange) { - // Can't select range with accepted rentals - setOwnerSelectionStart(null); - setOwnerSelectionStartHour(null); - return; - } - - // Create unavailable period - const newPeriod: UnavailablePeriod = { - id: Date.now().toString(), - startDate: finalStart, - endDate: finalEnd, - startTime: `${startHour.toString().padStart(2, '0')}:00`, - endTime: `${endHour.toString().padStart(2, '0')}:59` - }; - - onPeriodsChange([...unavailablePeriods, newPeriod]); - setOwnerSelectionStart(null); - setOwnerSelectionStartHour(null); - } - }; - - const formatDate = (date: Date) => { - return date.toLocaleDateString('en-US', { - month: 'short', - day: 'numeric', - year: 'numeric' - }); - }; - - const renderMonthView = () => { - const days = getDaysInMonth(currentDate); - const monthName = currentDate.toLocaleDateString('en-US', { month: 'long', year: 'numeric' }); - - return ( - <> -
{monthName}
-
- {['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'].map(day => ( -
- {day} -
- ))} - {days.map((date, index) => { - let className = 'p-2 text-center'; - let title = ''; - let backgroundColor = undefined; - - if (date) { - className += ' border cursor-pointer'; - - if (mode === 'owner') { - // OWNER VIEW - const acceptedRental = unavailablePeriods.find(p => - p.isAcceptedRental && isDateInPeriod(date, p) - ); - const isOwnerStart = ownerSelectionStart && date.getTime() === ownerSelectionStart.getTime(); - - if (acceptedRental) { - className += ' text-white'; - title = 'Booked - This date has an accepted rental'; - backgroundColor = '#6f42c1'; - } else if (isOwnerStart) { - className += ' bg-warning text-dark'; - title = 'Click another date to mark range as unavailable'; - } else if (isDateFullyUnavailable(date)) { - className += ' bg-danger text-white'; - title = 'Unavailable - Click to make available'; - } else if (isDatePartiallyUnavailable(date)) { - className += ' text-dark'; - title = 'Partially unavailable - Switch to week/day view for details'; - backgroundColor = '#ffeb3b'; - } else { - className += ' bg-light'; - title = ownerSelectionStart ? 'Click to complete selection' : 'Click to start selecting unavailable dates'; - } - } else { - // RENTER VIEW - let isInSelectedPeriod = false; - if (selectedRentalPeriod) { - // Special handling for rental selections with specific end times - if (selectedRentalPeriod.endTime === '00:00') { - // If end time is midnight, don't include the end date in highlighting - const start = new Date(selectedRentalPeriod.startDate); - const end = new Date(selectedRentalPeriod.endDate); - const checkDate = new Date(date); - - start.setHours(0, 0, 0, 0); - end.setHours(0, 0, 0, 0); - checkDate.setHours(0, 0, 0, 0); - - // Exclude the end date if end time is 00:00 - isInSelectedPeriod = checkDate >= start && checkDate < end; - } else { - isInSelectedPeriod = isDateInPeriod(date, selectedRentalPeriod); - } - } - const isSelectionStart = selectionStart && date.getTime() === selectionStart.getTime(); - - if (isDateFullyUnavailable(date)) { - className += ' bg-danger text-white'; - } else if (isDatePartiallyUnavailable(date)) { - className += ' text-dark'; - backgroundColor = '#ffeb3b'; - } else if (isInSelectedPeriod) { - className += ' bg-success text-white'; - } else if (isSelectionStart) { - className += ' bg-success bg-opacity-50 text-dark'; - } else { - className += ' bg-light'; - } - } - } - - return ( -
date && handleDateClick(date)} - style={{ - minHeight: '40px', - cursor: date ? 'pointer' : 'default', - backgroundColor: backgroundColor - }} - title={mode === 'owner' && date ? title : ''} - > - {date?.getDate()} -
- ); - })} -
- - ); - }; - - const renderWeekView = () => { - const days = getWeekDays(currentDate); - const weekRange = `${formatDate(days[0])} - ${formatDate(days[6])}`; - const hours = Array.from({ length: 24 }, (_, i) => i); - - return ( - <> -
{weekRange}
-
- - - - - {days.map((date, index) => ( - - ))} - - - - {hours.map(hour => ( - - - {days.map((date, dayIndex) => { - const isUnavailable = isHourUnavailable(date, hour); - const isClickable = mode === 'owner'; - const isOwnerSelectionStart = mode === 'owner' && ownerSelectionStart && - date.toDateString() === ownerSelectionStart.toDateString() && - hour === ownerSelectionStartHour; - - // Check if this hour is in the selected period - let isInSelectedPeriod = false; - if (mode === 'renter' && selectedRentalPeriod) { - const cellDate = new Date(date); - cellDate.setHours(0, 0, 0, 0); - const selStartDate = new Date(selectedRentalPeriod.startDate); - selStartDate.setHours(0, 0, 0, 0); - const selEndDate = new Date(selectedRentalPeriod.endDate); - selEndDate.setHours(0, 0, 0, 0); - - // Check if date is in range - if (cellDate >= selStartDate && cellDate <= selEndDate) { - // If times are specified, check hour range - if (selectedRentalPeriod.startTime && selectedRentalPeriod.endTime) { - const [startHour] = selectedRentalPeriod.startTime.split(':').map(Number); - const [endHour] = selectedRentalPeriod.endTime.split(':').map(Number); - - if (cellDate.getTime() === selStartDate.getTime() && cellDate.getTime() === selEndDate.getTime()) { - // Same day - check if hour is in range (end hour is exclusive) - isInSelectedPeriod = hour >= startHour && hour < endHour; - } else if (cellDate.getTime() === selStartDate.getTime()) { - // Start day - check if hour is after or at start hour - isInSelectedPeriod = hour >= startHour; - } else if (cellDate.getTime() === selEndDate.getTime()) { - // End day - check if hour is before end hour (exclusive) - isInSelectedPeriod = hour < endHour; - } else { - // Middle day - all hours selected - isInSelectedPeriod = true; - } - } else { - isInSelectedPeriod = true; - } - } - } - - const isSelectionStart = mode === 'renter' && selectionStart && - date.toDateString() === selectionStart.toDateString() && - hour === selectionStartHour; - - let cellClass = 'text-center p-1'; - if (isUnavailable) { - cellClass += ' bg-danger text-white'; - } else if (isInSelectedPeriod) { - cellClass += ' bg-success text-white'; - } else if (isOwnerSelectionStart) { - cellClass += ' bg-warning text-dark'; - } else if (isSelectionStart) { - cellClass += ' bg-success bg-opacity-50 text-dark'; - } else { - cellClass += ' bg-light'; - } - cellClass += (isClickable || (mode === 'renter' && !isUnavailable)) ? ' cursor-pointer' : ''; - - return ( - - ); - })} - - ))} - -
Time -
{date.toLocaleDateString('en-US', { weekday: 'short' })}
-
{date.getDate()}
-
- {hour === 0 ? '12 AM' : hour === 12 ? '12 PM' : hour < 12 ? `${hour} AM` : `${hour - 12} PM`} - { - if (mode === 'owner') { - toggleHourAvailability(date, hour); - } else if (mode === 'renter' && !isUnavailable) { - handleHourClick(date, hour); - } - }} - style={{ - cursor: (isClickable || (mode === 'renter' && !isUnavailable)) ? 'pointer' : 'default', - height: '40px', - borderLeft: dayIndex === 0 ? '1px solid #dee2e6' : 'none' - }} - title={ - mode === 'owner' - ? (isUnavailable ? 'Click to make this hour available' : (ownerSelectionStartHour !== null ? 'Click to complete selection' : 'Click to start selecting unavailable hours')) - : '' - } - > - {isUnavailable && ×} -
-
- - ); - }; - - const renderDayView = () => { - const dayName = currentDate.toLocaleDateString('en-US', { - weekday: 'long', - month: 'long', - day: 'numeric', - year: 'numeric' - }); - const hours = Array.from({ length: 24 }, (_, i) => i); - - return ( - <> -
{dayName}
-
- - - {hours.map(hour => { - const isUnavailable = isHourUnavailable(currentDate, hour); - const isClickable = mode === 'owner'; - const isOwnerSelectionStart = mode === 'owner' && ownerSelectionStart && - currentDate.toDateString() === ownerSelectionStart.toDateString() && - hour === ownerSelectionStartHour; - - // Check if this hour is in the selected period - let isInSelectedPeriod = false; - if (mode === 'renter' && selectedRentalPeriod) { - const cellDate = new Date(currentDate); - cellDate.setHours(0, 0, 0, 0); - const selStartDate = new Date(selectedRentalPeriod.startDate); - selStartDate.setHours(0, 0, 0, 0); - const selEndDate = new Date(selectedRentalPeriod.endDate); - selEndDate.setHours(0, 0, 0, 0); - - // Check if date is in range - if (cellDate >= selStartDate && cellDate <= selEndDate) { - // If times are specified, check hour range - if (selectedRentalPeriod.startTime && selectedRentalPeriod.endTime) { - const [startHour] = selectedRentalPeriod.startTime.split(':').map(Number); - const [endHour] = selectedRentalPeriod.endTime.split(':').map(Number); - - if (cellDate.getTime() === selStartDate.getTime() && cellDate.getTime() === selEndDate.getTime()) { - // Same day - check if hour is in range (end hour is exclusive) - isInSelectedPeriod = hour >= startHour && hour < endHour; - } else if (cellDate.getTime() === selStartDate.getTime()) { - // Start day - check if hour is after or at start hour - isInSelectedPeriod = hour >= startHour; - } else if (cellDate.getTime() === selEndDate.getTime()) { - // End day - check if hour is before end hour (exclusive) - isInSelectedPeriod = hour < endHour; - } else { - // Middle day - all hours selected - isInSelectedPeriod = true; - } - } else { - isInSelectedPeriod = true; - } - } - } - - const isSelectionStart = mode === 'renter' && selectionStart && - currentDate.toDateString() === selectionStart.toDateString() && - hour === selectionStartHour; - - let cellClass = 'text-center p-3'; - if (isUnavailable) { - cellClass += ' bg-danger text-white'; - } else if (isInSelectedPeriod) { - cellClass += ' bg-success text-white'; - } else if (isOwnerSelectionStart) { - cellClass += ' bg-warning text-dark'; - } else if (isSelectionStart) { - cellClass += ' bg-success bg-opacity-50 text-dark'; - } else { - cellClass += ' bg-light'; - } - cellClass += (isClickable || (mode === 'renter' && !isUnavailable)) ? ' cursor-pointer' : ''; - - return ( - - - - - ); - })} - -
- {hour === 0 ? '12:00 AM' : hour === 12 ? '12:00 PM' : hour < 12 ? `${hour}:00 AM` : `${hour - 12}:00 PM`} - { - if (mode === 'owner') { - toggleHourAvailability(currentDate, hour); - } else if (mode === 'renter' && !isUnavailable) { - handleHourClick(currentDate, hour); - } - }} - style={{ cursor: (isClickable || (mode === 'renter' && !isUnavailable)) ? 'pointer' : 'default' }} - title={ - mode === 'owner' - ? (isUnavailable ? 'Click to make this hour available' : (ownerSelectionStartHour !== null ? 'Click to complete selection' : 'Click to start selecting unavailable hours')) - : '' - } - > - {isUnavailable ? 'Unavailable' : 'Available'} -
-
- - ); - }; - - const navigateDate = (direction: 'prev' | 'next') => { - const newDate = new Date(currentDate); - - switch (viewType) { - case 'month': - newDate.setMonth(newDate.getMonth() + (direction === 'next' ? 1 : -1)); - break; - case 'week': - newDate.setDate(newDate.getDate() + (direction === 'next' ? 7 : -7)); - break; - case 'day': - newDate.setDate(newDate.getDate() + (direction === 'next' ? 1 : -1)); - break; - } - - setCurrentDate(newDate); - }; - - return ( -
-
-
- - - -
- -
- - -
-
- -
- {viewType === 'month' && renderMonthView()} - {viewType === 'week' && renderWeekView()} - {viewType === 'day' && renderDayView()} -
- -
-
- Available - {mode === 'renter' && selectedRentalPeriod && ( - Your Selection - )} - {mode === 'owner' && ownerSelectionStart && ( - Selection Start - )} - {mode === 'owner' && ( - Booked Rental - )} - {viewType === 'month' && ( - Partially Unavailable - )} - {mode === 'owner' ? 'Marked Unavailable' : 'Not Available'} -
-
-
- ); -}; - -export default AvailabilityCalendar; \ No newline at end of file diff --git a/frontend/src/components/Navbar.tsx b/frontend/src/components/Navbar.tsx index d0c9457..b68c741 100644 --- a/frontend/src/components/Navbar.tsx +++ b/frontend/src/components/Navbar.tsx @@ -148,18 +148,18 @@ const Navbar: React.FC = () => {
  • - Rentals + Renting
  • - Listings + Owning
  • - Requests + Looking For
  • diff --git a/frontend/src/components/ReviewModal.tsx b/frontend/src/components/ReviewModal.tsx index 0fa67ee..dec0c46 100644 --- a/frontend/src/components/ReviewModal.tsx +++ b/frontend/src/components/ReviewModal.tsx @@ -2,16 +2,17 @@ import React, { useState } from 'react'; import { rentalAPI } from '../services/api'; import { Rental } from '../types'; -interface ReviewModalProps { +interface ReviewItemModalProps { show: boolean; onClose: () => void; rental: Rental; onSuccess: () => void; } -const ReviewModal: React.FC = ({ show, onClose, rental, onSuccess }) => { +const ReviewItemModal: React.FC = ({ show, onClose, rental, onSuccess }) => { const [rating, setRating] = useState(5); const [review, setReview] = useState(''); + const [privateMessage, setPrivateMessage] = useState(''); const [submitting, setSubmitting] = useState(false); const [error, setError] = useState(null); @@ -21,16 +22,25 @@ const ReviewModal: React.FC = ({ show, onClose, rental, onSucc setSubmitting(true); try { - await rentalAPI.addReview(rental.id, { + const response = await rentalAPI.reviewItem(rental.id, { rating, - review + review, + privateMessage }); // Reset form setRating(5); setReview(''); + setPrivateMessage(''); onSuccess(); onClose(); + + // Show success message based on review visibility + if (response.data.reviewVisible) { + alert('Review published successfully!'); + } else { + alert('Review submitted! It will be published when both parties have reviewed or after 10 minutes.'); + } } catch (err: any) { setError(err.response?.data?.error || 'Failed to submit review'); } finally { @@ -49,7 +59,7 @@ const ReviewModal: React.FC = ({ show, onClose, rental, onSucc
    -
    Review Your Rental
    +
    Review Item
    @@ -96,7 +106,9 @@ const ReviewModal: React.FC = ({ show, onClose, rental, onSucc
    - + - Tell others about the item condition, owner communication, and overall experience + This will be visible to everyone. Tell others about the item condition, owner communication, and overall experience. + +
    + +
    + + + + This message will only be visible to the owner. Use this for specific feedback or suggestions.
    @@ -143,4 +173,4 @@ const ReviewModal: React.FC = ({ show, onClose, rental, onSucc ); }; -export default ReviewModal; \ No newline at end of file +export default ReviewItemModal; \ No newline at end of file diff --git a/frontend/src/components/ReviewRenterModal.tsx b/frontend/src/components/ReviewRenterModal.tsx new file mode 100644 index 0000000..b613ca8 --- /dev/null +++ b/frontend/src/components/ReviewRenterModal.tsx @@ -0,0 +1,176 @@ +import React, { useState } from 'react'; +import { rentalAPI } from '../services/api'; +import { Rental } from '../types'; + +interface ReviewRenterModalProps { + show: boolean; + onClose: () => void; + rental: Rental; + onSuccess: () => void; +} + +const ReviewRenterModal: React.FC = ({ show, onClose, rental, onSuccess }) => { + const [rating, setRating] = useState(5); + const [review, setReview] = useState(''); + const [privateMessage, setPrivateMessage] = useState(''); + const [submitting, setSubmitting] = useState(false); + const [error, setError] = useState(null); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(null); + setSubmitting(true); + + try { + const response = await rentalAPI.reviewRenter(rental.id, { + rating, + review, + privateMessage + }); + + // Reset form + setRating(5); + setReview(''); + setPrivateMessage(''); + onSuccess(); + onClose(); + + // Show success message based on review visibility + if (response.data.reviewVisible) { + alert('Review published successfully!'); + } else { + alert('Review submitted! It will be published when both parties have reviewed or after 10 minutes.'); + } + } catch (err: any) { + setError(err.response?.data?.error || 'Failed to submit review'); + } finally { + setSubmitting(false); + } + }; + + const handleStarClick = (value: number) => { + setRating(value); + }; + + if (!show) return null; + + return ( +
    +
    +
    +
    +
    Review Renter
    + +
    + +
    + {rental.renter && ( +
    +
    {rental.renter.firstName} {rental.renter.lastName}
    + + Rental period: {new Date(rental.startDate).toLocaleDateString()} to {new Date(rental.endDate).toLocaleDateString()} + +
    + )} + + {error && ( +
    + {error} +
    + )} + +
    + +
    + {[1, 2, 3, 4, 5].map((star) => ( + + ))} +
    +
    + + {rating === 1 && 'Poor'} + {rating === 2 && 'Fair'} + {rating === 3 && 'Good'} + {rating === 4 && 'Very Good'} + {rating === 5 && 'Excellent'} + +
    +
    + +
    + + + + This will be visible to everyone. Consider communication, condition of returned item, timeliness, and overall experience. + +
    + +
    + + + + This message will only be visible to the renter. Use this for specific feedback or suggestions. + +
    +
    +
    + + +
    + +
    +
    +
    + ); +}; + +export default ReviewRenterModal; \ No newline at end of file diff --git a/frontend/src/pages/ItemDetail.tsx b/frontend/src/pages/ItemDetail.tsx index c567a45..ef4a5d0 100644 --- a/frontend/src/pages/ItemDetail.tsx +++ b/frontend/src/pages/ItemDetail.tsx @@ -110,15 +110,80 @@ const ItemDetail: React.FC = () => { setTotalCost(cost); }; - const generateTimeOptions = () => { + const generateTimeOptions = (item: Item | null, selectedDate: string) => { const options = []; + let availableAfter = "00:00"; + let availableBefore = "23:59"; + + console.log('generateTimeOptions called with:', { + itemId: item?.id, + selectedDate, + hasItem: !!item + }); + + // Determine time constraints only if we have both item and a valid selected date + if (item && selectedDate && selectedDate.trim() !== "") { + const date = new Date(selectedDate); + const dayName = date.toLocaleDateString('en-US', { weekday: 'long' }).toLowerCase() as + 'sunday' | 'monday' | 'tuesday' | 'wednesday' | 'thursday' | 'friday' | 'saturday'; + + console.log('Date analysis:', { + selectedDate, + dayName, + specifyTimesPerDay: item.specifyTimesPerDay, + hasWeeklyTimes: !!item.weeklyTimes, + globalAvailableAfter: item.availableAfter, + globalAvailableBefore: item.availableBefore + }); + + // Use day-specific times if available + if (item.specifyTimesPerDay && item.weeklyTimes && item.weeklyTimes[dayName]) { + const dayTimes = item.weeklyTimes[dayName]; + availableAfter = dayTimes.availableAfter; + availableBefore = dayTimes.availableBefore; + console.log('Using day-specific times:', { availableAfter, availableBefore }); + } + // Otherwise use global times + else if (item.availableAfter && item.availableBefore) { + availableAfter = item.availableAfter; + availableBefore = item.availableBefore; + console.log('Using global times:', { availableAfter, availableBefore }); + } else { + console.log('No time constraints found, using default 24-hour availability'); + } + } else { + console.log('Missing item or selectedDate, using default 24-hour availability'); + } + for (let hour = 0; hour < 24; hour++) { const time24 = `${hour.toString().padStart(2, "0")}:00`; - const hour12 = hour === 0 ? 12 : hour > 12 ? hour - 12 : hour; - const period = hour < 12 ? "AM" : "PM"; - const time12 = `${hour12}:00 ${period}`; - options.push({ value: time24, label: time12 }); + + // Ensure consistent format for comparison (normalize to HH:MM) + const normalizedAvailableAfter = availableAfter.length === 5 ? availableAfter : availableAfter + ":00"; + const normalizedAvailableBefore = availableBefore.length === 5 ? availableBefore : availableBefore + ":00"; + + // Check if this time is within the available range + if (time24 >= normalizedAvailableAfter && time24 <= normalizedAvailableBefore) { + const hour12 = hour === 0 ? 12 : hour > 12 ? hour - 12 : hour; + const period = hour < 12 ? "AM" : "PM"; + const time12 = `${hour12}:00 ${period}`; + options.push({ value: time24, label: time12 }); + } } + + console.log('Time filtering results:', { + availableAfter, + availableBefore, + optionsGenerated: options.length, + firstFewOptions: options.slice(0, 3) + }); + + // If no options are available, return at least one option to prevent empty dropdown + if (options.length === 0) { + console.log('No valid time options found, showing Not Available'); + options.push({ value: "00:00", label: "Not Available" }); + } + return options; }; @@ -126,6 +191,34 @@ const ItemDetail: React.FC = () => { calculateTotalCost(); }, [rentalDates, item]); + // Validate and adjust selected times based on item availability + useEffect(() => { + if (!item) return; + + const validateAndAdjustTime = (date: string, currentTime: string) => { + if (!date) return currentTime; + + const availableOptions = generateTimeOptions(item, date); + if (availableOptions.length === 0) return currentTime; + + // If current time is not in available options, use the first available time + const isCurrentTimeValid = availableOptions.some(option => option.value === currentTime); + return isCurrentTimeValid ? currentTime : availableOptions[0].value; + }; + + const adjustedStartTime = validateAndAdjustTime(rentalDates.startDate, rentalDates.startTime); + const adjustedEndTime = validateAndAdjustTime(rentalDates.endDate || rentalDates.startDate, rentalDates.endTime); + + // Update state if times have changed + if (adjustedStartTime !== rentalDates.startTime || adjustedEndTime !== rentalDates.endTime) { + setRentalDates(prev => ({ + ...prev, + startTime: adjustedStartTime, + endTime: adjustedEndTime + })); + } + }, [item, rentalDates.startDate, rentalDates.endDate]); + if (loading) { return (
    @@ -392,8 +485,9 @@ const ItemDetail: React.FC = () => { handleDateTimeChange("startTime", e.target.value) } style={{ flex: '1 1 50%' }} + disabled={!!(rentalDates.startDate && generateTimeOptions(item, rentalDates.startDate).every(opt => opt.label === "Not Available"))} > - {generateTimeOptions().map((option) => ( + {generateTimeOptions(item, rentalDates.startDate).map((option) => ( @@ -425,8 +519,9 @@ const ItemDetail: React.FC = () => { handleDateTimeChange("endTime", e.target.value) } style={{ flex: '1 1 50%' }} + disabled={!!((rentalDates.endDate || rentalDates.startDate) && generateTimeOptions(item, rentalDates.endDate || rentalDates.startDate).every(opt => opt.label === "Not Available"))} > - {generateTimeOptions().map((option) => ( + {generateTimeOptions(item, rentalDates.endDate || rentalDates.startDate).map((option) => ( diff --git a/frontend/src/pages/MyListings.tsx b/frontend/src/pages/MyListings.tsx index 7759f1d..9ca5958 100644 --- a/frontend/src/pages/MyListings.tsx +++ b/frontend/src/pages/MyListings.tsx @@ -4,18 +4,43 @@ import { useAuth } from "../contexts/AuthContext"; import api from "../services/api"; import { Item, Rental } from "../types"; import { rentalAPI } from "../services/api"; +import ReviewRenterModal from "../components/ReviewRenterModal"; const MyListings: 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 = (dateString: string, timeString?: string) => { + const date = new Date(dateString).toLocaleDateString(); + const formattedTime = formatTime(timeString); + return formattedTime ? `${date} at ${formattedTime}` : date; + }; + const { user } = useAuth(); const navigate = useNavigate(); const [listings, setListings] = useState([]); - const [rentals, setRentals] = useState([]); + const [ownerRentals, setOwnerRentals] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(""); + // Owner rental management state + const [showReviewRenterModal, setShowReviewRenterModal] = useState(false); + const [selectedRentalForReview, setSelectedRentalForReview] = useState(null); useEffect(() => { fetchMyListings(); - fetchRentalRequests(); + fetchOwnerRentals(); }, [user]); const fetchMyListings = async () => { @@ -23,7 +48,7 @@ const MyListings: React.FC = () => { try { setLoading(true); - setError(""); // Clear any previous errors + setError(""); const response = await api.get("/items"); // Filter items to only show ones owned by current user @@ -33,7 +58,6 @@ const MyListings: React.FC = () => { setListings(myItems); } catch (err: any) { console.error("Error fetching listings:", err); - // Only show error for actual API failures if (err.response && err.response.status >= 500) { setError("Failed to get your listings. Please try again later."); } @@ -70,40 +94,64 @@ const MyListings: React.FC = () => { } }; - const fetchRentalRequests = async () => { - if (!user) return; - + const fetchOwnerRentals = async () => { try { const response = await rentalAPI.getMyListings(); - setRentals(response.data); - } catch (err) { - console.error("Error fetching rental requests:", err); + console.log("Owner rentals data from backend:", response.data); + setOwnerRentals(response.data); + } catch (err: any) { + console.error("Failed to fetch owner rentals:", err); } }; + // Owner functionality handlers const handleAcceptRental = async (rentalId: string) => { try { await rentalAPI.updateRentalStatus(rentalId, "confirmed"); - // Refresh the rentals list - fetchRentalRequests(); + fetchOwnerRentals(); } catch (err) { console.error("Failed to accept rental request:", err); + alert("Failed to accept rental request"); } }; const handleRejectRental = async (rentalId: string) => { try { - await api.put(`/rentals/${rentalId}/status`, { - status: "cancelled", - rejectionReason: "Request declined by owner", - }); - // Refresh the rentals list - fetchRentalRequests(); + await rentalAPI.updateRentalStatus(rentalId, "cancelled"); + fetchOwnerRentals(); } catch (err) { console.error("Failed to reject rental request:", err); + alert("Failed to reject rental request"); } }; + const handleCompleteClick = async (rental: Rental) => { + try { + console.log('Marking rental as completed:', rental.id); + await rentalAPI.markAsCompleted(rental.id); + + setSelectedRentalForReview(rental); + setShowReviewRenterModal(true); + + 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)); + } + }; + + const handleReviewRenterSuccess = () => { + fetchOwnerRentals(); + }; + + // Filter owner rentals + 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 (
    @@ -119,7 +167,7 @@ const MyListings: React.FC = () => { return (
    -

    Listings

    +

    My Listings

    Add New Item @@ -131,26 +179,103 @@ const MyListings: React.FC = () => {
    )} - {(() => { - const pendingCount = rentals.filter( - (r) => r.status === "pending" - ).length; + {/* Rental Requests Section - Moved to top for priority */} + {allOwnerRentals.length > 0 && ( +
    +

    + + Rental Requests ({allOwnerRentals.length}) +

    +
    + {allOwnerRentals.map((rental) => ( +
    +
    + {rental.item?.images && rental.item.images[0] && ( + {rental.item.name} + )} +
    +
    + {rental.item ? rental.item.name : "Item Unavailable"} +
    - if (pendingCount > 0) { - return ( -
    - - You have {pendingCount} pending rental request - {pendingCount > 1 ? "s" : ""} to review. -
    - ); - } - return null; - })()} + {rental.renter && ( +

    + Renter: {rental.renter.firstName} {rental.renter.lastName} +

    + )} +
    + + {rental.status.charAt(0).toUpperCase() + rental.status.slice(1)} + +
    + +

    + Period: +
    + {formatDateTime(rental.startDate, rental.startTime)} - {formatDateTime(rental.endDate, rental.endTime)} +

    + +

    + Total: ${rental.totalAmount} +

    + + {rental.itemPrivateMessage && rental.itemReviewVisible && ( +
    + Private Note from Renter: +
    + {rental.itemPrivateMessage} +
    + )} + +
    + {rental.status === "pending" && ( + <> + + + + )} + {(rental.status === "active" || rental.status === "confirmed") && ( + + )} +
    +
    +
    +
    + ))} +
    +
    + )} + +

    + + My Items ({listings.length}) +

    + {listings.length === 0 ? (

    You haven't listed any items yet.

    @@ -162,210 +287,132 @@ const MyListings: React.FC = () => {
    {listings.map((item) => (
    -
    ) => { - const target = e.target as HTMLElement; - if ( - target.closest("button") || - target.closest("a") || - target.closest(".rental-requests") - ) { - return; - } - navigate(`/items/${item.id}`); - }} - > - {item.images && item.images[0] && ( - {item.name} - )} -
    -
    {item.name}
    -

    - {item.description} -

    +
    ) => { + const target = e.target as HTMLElement; + if (target.closest("button") || target.closest("a")) { + return; + } + navigate(`/items/${item.id}`); + }} + > + {item.images && item.images[0] && ( + {item.name} + )} +
    +
    {item.name}
    +

    + {item.description} +

    -
    - - {item.availability ? "Available" : "Not Available"} - -
    - -
    - {item.pricePerDay && ( -
    - ${item.pricePerDay}/day -
    - )} - {item.pricePerHour && ( -
    - ${item.pricePerHour}/hour -
    - )} -
    - -
    - - Edit - - - -
    +
    + + {item.availability ? "Available" : "Not Available"} + +
    +
    {(() => { - const pendingRentals = rentals.filter( - (r) => r.itemId === item.id && r.status === "pending" - ); - const acceptedRentals = rentals.filter( - (r) => - r.itemId === item.id && - ["confirmed", "active"].includes(r.status) - ); + 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); - if ( - pendingRentals.length > 0 || - acceptedRentals.length > 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 ( -
    - {pendingRentals.length > 0 && ( - <> -
    - Pending - Requests ({pendingRentals.length}) -
    - {pendingRentals.map((rental) => ( -
    -
    -
    - - {rental.renter?.firstName}{" "} - {rental.renter?.lastName} - -
    - {new Date( - rental.startDate - ).toLocaleDateString()}{" "} - -{" "} - {new Date( - rental.endDate - ).toLocaleDateString()} -
    - - ${rental.totalAmount} - -
    -
    - - -
    -
    -
    - ))} - - )} - - {acceptedRentals.length > 0 && ( - <> -
    - {" "} - Accepted Rentals ({acceptedRentals.length}) -
    - {acceptedRentals.map((rental) => ( -
    -
    -
    -
    - - {rental.renter?.firstName}{" "} - {rental.renter?.lastName} - -
    - {new Date( - rental.startDate - ).toLocaleDateString()}{" "} - -{" "} - {new Date( - rental.endDate - ).toLocaleDateString()} -
    - - ${rental.totalAmount} - -
    - - {rental.status === "active" - ? "Active" - : "Confirmed"} - -
    -
    -
    - ))} - - )} +
    + Free to Borrow
    ); } - return null; + + return ( + <> + {item.pricePerDay && Number(item.pricePerDay) > 0 && ( +
    + ${item.pricePerDay}/day +
    + )} + {item.pricePerHour && Number(item.pricePerHour) > 0 && ( +
    + ${item.pricePerHour}/hour +
    + )} + {item.pricePerWeek && Number(item.pricePerWeek) > 0 && ( +
    + ${item.pricePerWeek}/week +
    + )} + {item.pricePerMonth && Number(item.pricePerMonth) > 0 && ( +
    + ${item.pricePerMonth}/month +
    + )} + + ); })()}
    + +
    + + Edit + + + +
    +
    ))}
    )} + + + {/* Review Modal */} + {selectedRentalForReview && ( + { + setShowReviewRenterModal(false); + setSelectedRentalForReview(null); + }} + rental={selectedRentalForReview} + onSuccess={handleReviewRenterSuccess} + /> + )}
    ); }; diff --git a/frontend/src/pages/MyRentals.tsx b/frontend/src/pages/MyRentals.tsx index 409ca7a..625bb34 100644 --- a/frontend/src/pages/MyRentals.tsx +++ b/frontend/src/pages/MyRentals.tsx @@ -3,15 +3,35 @@ import { Link } from "react-router-dom"; import { useAuth } from "../contexts/AuthContext"; import { rentalAPI } from "../services/api"; import { Rental } from "../types"; -import ReviewModal from "../components/ReviewModal"; +import ReviewItemModal from "../components/ReviewModal"; import ConfirmationModal from "../components/ConfirmationModal"; const MyRentals: 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 = (dateString: string, timeString?: string) => { + const date = new Date(dateString).toLocaleDateString(); + const formattedTime = formatTime(timeString); + return formattedTime ? `${date} at ${formattedTime}` : date; + }; + const { user } = useAuth(); const [rentals, setRentals] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); - const [activeTab, setActiveTab] = useState<"active" | "past">("active"); const [showReviewModal, setShowReviewModal] = useState(false); const [selectedRental, setSelectedRental] = useState(null); const [showCancelModal, setShowCancelModal] = useState(false); @@ -25,6 +45,10 @@ const MyRentals: React.FC = () => { const fetchRentals = async () => { try { const response = await rentalAPI.getMyRentals(); + console.log("MyRentals data from backend:", response.data); + if (response.data.length > 0) { + console.log("First rental object:", response.data[0]); + } setRentals(response.data); } catch (err: any) { setError(err.response?.data?.message || "Failed to fetch rentals"); @@ -44,7 +68,7 @@ const MyRentals: React.FC = () => { setCancelling(true); try { await rentalAPI.updateRentalStatus(rentalToCancel, "cancelled"); - fetchRentals(); // Refresh the list + fetchRentals(); setShowCancelModal(false); setRentalToCancel(null); } catch (err: any) { @@ -60,19 +84,14 @@ const MyRentals: React.FC = () => { }; const handleReviewSuccess = () => { - fetchRentals(); // Refresh to show the review has been added + fetchRentals(); alert("Thank you for your review!"); }; - // Filter rentals based on status - const activeRentals = rentals.filter((r) => + // Filter rentals - only show active rentals (pending, confirmed, active) + const renterActiveRentals = rentals.filter((r) => ["pending", "confirmed", "active"].includes(r.status) ); - const pastRentals = rentals.filter((r) => - ["completed", "cancelled"].includes(r.status) - ); - - const displayedRentals = activeTab === "active" ? activeRentals : pastRentals; if (loading) { return ( @@ -100,156 +119,134 @@ const MyRentals: React.FC = () => {

    My Rentals

    -
      -
    • - -
    • -
    • - -
    • -
    - - {displayedRentals.length === 0 ? ( + {renterActiveRentals.length === 0 ? (
    -

    - {activeTab === "active" - ? "You don't have any active rentals." - : "You don't have any past rentals."} -

    - +
    No Active Rental Requests
    +

    You don't have any rental requests at the moment.

    + Browse Items to Rent
    ) : (
    - {displayedRentals.map((rental) => ( -
    - ) => { - const target = e.target as HTMLElement; - if (!rental.item || target.closest("button")) { - e.preventDefault(); - } - }} - > -
    ( +
    + ) => { + const target = e.target as HTMLElement; + if (!rental.item || target.closest("button")) { + e.preventDefault(); + } + }} > - {rental.item?.images && rental.item.images[0] && ( - {rental.item.name} - )} -
    -
    - {rental.item ? rental.item.name : "Item Unavailable"} -
    - -
    - - {rental.status.charAt(0).toUpperCase() + - rental.status.slice(1)} - - {rental.paymentStatus === "paid" && ( - Paid - )} -
    - -

    - Rental Period: -
    - {new Date(rental.startDate).toLocaleDateString()} -{" "} - {new Date(rental.endDate).toLocaleDateString()} -

    - -

    - Total: ${rental.totalAmount} -

    - -

    - Delivery:{" "} - {rental.deliveryMethod === "pickup" - ? "Pick-up" - : "Delivery"} -

    - - {rental.owner && ( -

    - Owner: {rental.owner.firstName}{" "} - {rental.owner.lastName} -

    +
    + {rental.item?.images && rental.item.images[0] && ( + {rental.item.name} )} +
    +
    + {rental.item ? rental.item.name : "Item Unavailable"} +
    - {rental.status === "cancelled" && - rental.rejectionReason && ( +
    + + {rental.status.charAt(0).toUpperCase() + rental.status.slice(1)} + + {rental.paymentStatus === "paid" && ( + Paid + )} +
    + +

    + Rental Period: +
    + Start: {formatDateTime(rental.startDate, rental.startTime)} +
    + End: {formatDateTime(rental.endDate, rental.endTime)} +

    + +

    + Total: ${rental.totalAmount} +

    + + {rental.owner && ( +

    + Owner: {rental.owner.firstName} {rental.owner.lastName} +

    + )} + + {rental.renterPrivateMessage && rental.renterReviewVisible && ( +
    + Private Note from Owner: +
    + {rental.renterPrivateMessage} +
    + )} + + {rental.status === "cancelled" && rental.rejectionReason && (
    - Rejection reason:{" "} - {rental.rejectionReason} + Rejection reason: {rental.rejectionReason}
    )} -
    - {rental.status === "pending" && ( - - )} - {rental.status === "completed" && !rental.rating && ( - - )} - {rental.status === "completed" && rental.rating && ( -
    - - Reviewed ({rental.rating}/5) -
    - )} +
    + {rental.status === "pending" && ( + + )} + {rental.status === "active" && !rental.itemRating && !rental.itemReviewSubmittedAt && ( + + )} + {rental.itemReviewSubmittedAt && !rental.itemReviewVisible && ( +
    + + Review Submitted +
    + )} + {rental.itemReviewVisible && rental.itemRating && ( +
    + + Review Published ({rental.itemRating}/5) +
    + )} + {rental.status === "completed" && rental.rating && !rental.itemRating && ( +
    + + Reviewed ({rental.rating}/5) +
    + )} +
    -
    - -
    - ))} + +
    + ))}
    )} + {/* Review Modal */} {selectedRental && ( - { setShowReviewModal(false); @@ -278,4 +275,4 @@ const MyRentals: React.FC = () => { ); }; -export default MyRentals; +export default MyRentals; \ No newline at end of file diff --git a/frontend/src/pages/Profile.tsx b/frontend/src/pages/Profile.tsx index 673ba65..8c2b435 100644 --- a/frontend/src/pages/Profile.tsx +++ b/frontend/src/pages/Profile.tsx @@ -4,6 +4,8 @@ import { userAPI, itemAPI, rentalAPI, addressAPI } from "../services/api"; import { User, Item, Rental, Address } from "../types"; import { getImageUrl } from "../utils/imageUrl"; import AvailabilitySettings from "../components/AvailabilitySettings"; +import ReviewItemModal from "../components/ReviewModal"; +import ReviewRenterModal from "../components/ReviewRenterModal"; const Profile: React.FC = () => { const { user, updateUser, logout } = useAuth(); @@ -59,12 +61,22 @@ const Profile: React.FC = () => { zipCode: "", country: "US", }); + + // Rental history state + const [pastRenterRentals, setPastRenterRentals] = useState([]); + const [pastOwnerRentals, setPastOwnerRentals] = useState([]); + const [rentalHistoryLoading, setRentalHistoryLoading] = useState(true); + const [showReviewModal, setShowReviewModal] = useState(false); + const [showReviewRenterModal, setShowReviewRenterModal] = useState(false); + const [selectedRental, setSelectedRental] = useState(null); + const [selectedRentalForReview, setSelectedRentalForReview] = useState(null); useEffect(() => { fetchProfile(); fetchStats(); fetchUserAddresses(); fetchUserAvailability(); + fetchRentalHistory(); }, []); const fetchUserAvailability = async () => { @@ -141,6 +153,73 @@ const Profile: React.FC = () => { } }; + // Helper functions for rental history + 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 ""; + } + }; + + const formatDateTime = (dateString: string, timeString?: string) => { + const date = new Date(dateString).toLocaleDateString(); + const formattedTime = formatTime(timeString); + return formattedTime ? `${date} at ${formattedTime}` : date; + }; + + const fetchRentalHistory = async () => { + try { + // Fetch past rentals as a renter + const renterResponse = await rentalAPI.getMyRentals(); + const pastRenterRentals = renterResponse.data.filter((r: Rental) => + ["completed", "cancelled"].includes(r.status) + ); + setPastRenterRentals(pastRenterRentals); + + // Fetch past rentals as an owner + const ownerResponse = await rentalAPI.getMyListings(); + const pastOwnerRentals = ownerResponse.data.filter((r: Rental) => + ["completed", "cancelled"].includes(r.status) + ); + setPastOwnerRentals(pastOwnerRentals); + } catch (err) { + console.error("Failed to fetch rental history:", err); + } finally { + setRentalHistoryLoading(false); + } + }; + + const handleReviewClick = (rental: Rental) => { + setSelectedRental(rental); + setShowReviewModal(true); + }; + + const handleReviewSuccess = () => { + fetchRentalHistory(); // Refresh to show updated review status + alert("Thank you for your review!"); + }; + + const handleCompleteClick = async (rental: Rental) => { + try { + await rentalAPI.markAsCompleted(rental.id); + setSelectedRentalForReview(rental); + setShowReviewRenterModal(true); + fetchRentalHistory(); // Refresh rental history + } catch (err: any) { + alert("Failed to mark rental as completed: " + (err.response?.data?.error || err.message)); + } + }; + + const handleReviewRenterSuccess = () => { + fetchRentalHistory(); // Refresh to show updated review status + }; + const handleChange = ( e: React.ChangeEvent ) => { @@ -472,6 +551,15 @@ const Profile: React.FC = () => { Owner Settings +
    )} + {/* Rental History Section */} + {activeSection === "rental-history" && ( +
    +

    Rental History

    + + {rentalHistoryLoading ? ( +
    +
    + Loading... +
    +
    + ) : ( + <> + {/* As Renter Section */} + {pastRenterRentals.length > 0 && ( +
    +
    + + As Renter ({pastRenterRentals.length}) +
    +
    + {pastRenterRentals.map((rental) => ( +
    +
    + {rental.item?.images && rental.item.images[0] && ( + {rental.item.name} + )} +
    +
    + {rental.item ? rental.item.name : "Item Unavailable"} +
    + +
    + + {rental.status.charAt(0).toUpperCase() + rental.status.slice(1)} + +
    + +

    + Period: +
    + Start: {formatDateTime(rental.startDate, rental.startTime)} +
    + End: {formatDateTime(rental.endDate, rental.endTime)} +

    + +

    + Total: ${rental.totalAmount} +

    + + {rental.owner && ( +

    + Owner: {rental.owner.firstName} {rental.owner.lastName} +

    + )} + + {rental.renterPrivateMessage && rental.renterReviewVisible && ( +
    + Private Note from Owner: +
    + {rental.renterPrivateMessage} +
    + )} + + {rental.status === "cancelled" && rental.rejectionReason && ( +
    + Rejection reason: {rental.rejectionReason} +
    + )} + +
    + {rental.status === "completed" && !rental.itemRating && !rental.itemReviewSubmittedAt && ( + + )} + {rental.itemReviewSubmittedAt && !rental.itemReviewVisible && ( +
    + + Review Submitted +
    + )} + {rental.itemReviewVisible && rental.itemRating && ( +
    + + Review Published ({rental.itemRating}/5) +
    + )} + {rental.status === "completed" && rental.rating && !rental.itemRating && ( +
    + + Reviewed ({rental.rating}/5) +
    + )} +
    +
    +
    +
    + ))} +
    +
    + )} + + {/* As Owner Section */} + {pastOwnerRentals.length > 0 && ( +
    +
    + + As Owner ({pastOwnerRentals.length}) +
    +
    + {pastOwnerRentals.map((rental) => ( +
    +
    + {rental.item?.images && rental.item.images[0] && ( + {rental.item.name} + )} +
    +
    + {rental.item ? rental.item.name : "Item Unavailable"} +
    + + {rental.renter && ( +

    + Renter: {rental.renter.firstName} {rental.renter.lastName} +

    + )} + +
    + + {rental.status.charAt(0).toUpperCase() + rental.status.slice(1)} + +
    + +

    + Period: +
    + {formatDateTime(rental.startDate, rental.startTime)} - {formatDateTime(rental.endDate, rental.endTime)} +

    + +

    + Total: ${rental.totalAmount} +

    + + {rental.itemPrivateMessage && rental.itemReviewVisible && ( +
    + Private Note from Renter: +
    + {rental.itemPrivateMessage} +
    + )} + +
    + {rental.status === "completed" && !rental.renterRating && !rental.renterReviewSubmittedAt && ( + + )} + {rental.renterReviewSubmittedAt && !rental.renterReviewVisible && ( +
    + + Review Submitted +
    + )} + {rental.renterReviewVisible && rental.renterRating && ( +
    + + Review Published ({rental.renterRating}/5) +
    + )} +
    +
    +
    +
    + ))} +
    +
    + )} + + {/* Empty State */} + {pastRenterRentals.length === 0 && pastOwnerRentals.length === 0 && ( +
    + +
    No Rental History
    +

    Your completed rentals and rental requests will appear here.

    +
    + )} + + )} +
    + )} + {/* Personal Information Section */} {activeSection === "personal-info" && (
    @@ -710,7 +1017,7 @@ const Profile: React.FC = () => { name="phone" value={formData.phone} onChange={handleChange} - placeholder="+1 (555) 123-4567" + placeholder="(123) 456-7890" disabled={!editing} />
    @@ -1022,6 +1329,31 @@ const Profile: React.FC = () => { )}
    + + {/* Review Modals */} + {selectedRental && ( + { + setShowReviewModal(false); + setSelectedRental(null); + }} + rental={selectedRental} + onSuccess={handleReviewSuccess} + /> + )} + + {selectedRentalForReview && ( + { + setShowReviewRenterModal(false); + setSelectedRentalForReview(null); + }} + rental={selectedRentalForReview} + onSuccess={handleReviewRenterSuccess} + /> + )}
    ); }; diff --git a/frontend/src/pages/RentItem.tsx b/frontend/src/pages/RentItem.tsx index 205ff70..2d5cc1a 100644 --- a/frontend/src/pages/RentItem.tsx +++ b/frontend/src/pages/RentItem.tsx @@ -312,7 +312,7 @@ const RentItem: React.FC = () => { name="cardName" value={formData.cardName} onChange={handleChange} - placeholder="John Doe" + placeholder="" required />
    diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts index 5edef57..5d05941 100644 --- a/frontend/src/services/api.ts +++ b/frontend/src/services/api.ts @@ -78,7 +78,10 @@ export const rentalAPI = { getMyListings: () => api.get("/rentals/my-listings"), updateRentalStatus: (id: string, status: string) => api.put(`/rentals/${id}/status`, { status }), - addReview: (id: string, data: any) => api.post(`/rentals/${id}/review`, data), + markAsCompleted: (id: string) => api.post(`/rentals/${id}/mark-completed`), + reviewRenter: (id: string, data: any) => api.post(`/rentals/${id}/review-renter`, data), + reviewItem: (id: string, data: any) => api.post(`/rentals/${id}/review-item`, data), + addReview: (id: string, data: any) => api.post(`/rentals/${id}/review`, data), // Legacy }; export const messageAPI = { diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index 1c68003..335664c 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -78,13 +78,6 @@ export interface Item { minimumRentalDays: number; maximumRentalDays?: number; needsTraining?: boolean; - unavailablePeriods?: Array<{ - id: string; - startDate: Date; - endDate: Date; - startTime?: string; - endTime?: string; - }>; availableAfter?: string; availableBefore?: string; specifyTimesPerDay?: boolean; @@ -110,6 +103,8 @@ export interface Rental { ownerId: string; startDate: string; endDate: string; + startTime?: string; + endTime?: string; totalAmount: number; status: "pending" | "confirmed" | "active" | "completed" | "cancelled"; paymentStatus: "pending" | "paid" | "refunded"; @@ -119,6 +114,18 @@ export interface Rental { rating?: number; review?: string; rejectionReason?: string; + // New review fields + itemRating?: number; + itemReview?: string; + itemReviewSubmittedAt?: string; + itemReviewVisible?: boolean; + renterRating?: number; + renterReview?: string; + renterReviewSubmittedAt?: string; + renterReviewVisible?: boolean; + // Private messages + itemPrivateMessage?: string; + renterPrivateMessage?: string; item?: Item; renter?: User; owner?: User;