date selection
This commit is contained in:
@@ -13,8 +13,9 @@ interface UnavailablePeriod {
|
||||
interface AvailabilityCalendarProps {
|
||||
unavailablePeriods: UnavailablePeriod[];
|
||||
onPeriodsChange: (periods: UnavailablePeriod[]) => void;
|
||||
priceType?: 'hour' | 'day';
|
||||
isRentalMode?: boolean;
|
||||
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';
|
||||
@@ -22,19 +23,20 @@ type ViewType = 'month' | 'week' | 'day';
|
||||
const AvailabilityCalendar: React.FC<AvailabilityCalendarProps> = ({
|
||||
unavailablePeriods,
|
||||
onPeriodsChange,
|
||||
priceType = 'hour',
|
||||
isRentalMode = false
|
||||
mode,
|
||||
onRentalSelect,
|
||||
selectedRentalPeriod: externalSelectedPeriod
|
||||
}) => {
|
||||
const [currentDate, setCurrentDate] = useState(new Date());
|
||||
const [viewType, setViewType] = useState<ViewType>('month');
|
||||
const [selectionStart, setSelectionStart] = useState<Date | null>(null);
|
||||
|
||||
// Reset to month view if priceType is day and current view is week/day
|
||||
useEffect(() => {
|
||||
if (priceType === 'day' && (viewType === 'week' || viewType === 'day')) {
|
||||
setViewType('month');
|
||||
}
|
||||
}, [priceType]);
|
||||
const [selectionStartHour, setSelectionStartHour] = useState<number | null>(null);
|
||||
const [internalSelectedPeriod, setInternalSelectedPeriod] = useState<UnavailablePeriod | null>(null);
|
||||
const [ownerSelectionStart, setOwnerSelectionStart] = useState<Date | null>(null);
|
||||
const [ownerSelectionStartHour, setOwnerSelectionStartHour] = useState<number | null>(null);
|
||||
|
||||
// Use external selection if provided, otherwise use internal
|
||||
const selectedRentalPeriod = externalSelectedPeriod || internalSelectedPeriod;
|
||||
|
||||
const getDaysInMonth = (date: Date) => {
|
||||
const year = date.getFullYear();
|
||||
@@ -77,10 +79,14 @@ const AvailabilityCalendar: React.FC<AvailabilityCalendarProps> = ({
|
||||
const isDateInPeriod = (date: Date, period: UnavailablePeriod) => {
|
||||
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);
|
||||
|
||||
// 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;
|
||||
};
|
||||
|
||||
@@ -158,157 +164,400 @@ const AvailabilityCalendar: React.FC<AvailabilityCalendarProps> = ({
|
||||
});
|
||||
};
|
||||
|
||||
const handleDateClick = (date: Date) => {
|
||||
// Check if this date has an accepted rental
|
||||
const hasAcceptedRental = unavailablePeriods.some(p =>
|
||||
p.isAcceptedRental && isDateInPeriod(date, p)
|
||||
);
|
||||
const handleHourClick = (date: Date, hour: number) => {
|
||||
if (mode !== 'renter') return;
|
||||
|
||||
if (hasAcceptedRental) {
|
||||
// Don't allow clicking on accepted rental dates
|
||||
return;
|
||||
}
|
||||
// Check if this hour is unavailable
|
||||
if (isHourUnavailable(date, hour)) return;
|
||||
|
||||
if (!isRentalMode) {
|
||||
toggleDateAvailability(date);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if clicking on an existing rental selection to clear it
|
||||
const existingRental = unavailablePeriods.find(p =>
|
||||
p.isRentalSelection && isDateInPeriod(date, p)
|
||||
);
|
||||
|
||||
if (existingRental) {
|
||||
// Clear the rental selection
|
||||
onPeriodsChange(unavailablePeriods.filter(p => p.id !== existingRental.id));
|
||||
setSelectionStart(null);
|
||||
return;
|
||||
}
|
||||
|
||||
// Two-click selection in rental mode
|
||||
if (!selectionStart) {
|
||||
// First click - set start date
|
||||
if (!selectionStart || !selectionStartHour) {
|
||||
// First click - set start date and hour
|
||||
setSelectionStart(date);
|
||||
setSelectionStartHour(hour);
|
||||
setInternalSelectedPeriod(null);
|
||||
} else {
|
||||
// Second click - create rental period
|
||||
const start = new Date(Math.min(selectionStart.getTime(), date.getTime()));
|
||||
const end = new Date(Math.max(selectionStart.getTime(), date.getTime()));
|
||||
// Second click - complete selection
|
||||
const startDate = new Date(selectionStart);
|
||||
const endDate = new Date(date);
|
||||
|
||||
// Check if any date in range is unavailable
|
||||
let currentDate = new Date(start);
|
||||
// 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 (currentDate <= end) {
|
||||
if (unavailablePeriods.some(p => !p.isRentalSelection && isDateInPeriod(currentDate, p))) {
|
||||
|
||||
while (current <= finalEnd) {
|
||||
const currentHour = current.getHours();
|
||||
if (isHourUnavailable(current, currentHour)) {
|
||||
hasUnavailable = true;
|
||||
break;
|
||||
}
|
||||
currentDate.setDate(currentDate.getDate() + 1);
|
||||
current.setHours(current.getHours() + 1);
|
||||
}
|
||||
|
||||
if (hasUnavailable) {
|
||||
// Range contains unavailable dates, reset selection
|
||||
// Range contains unavailable hours, reset
|
||||
setSelectionStart(null);
|
||||
setSelectionStartHour(null);
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear existing rental selections and add new one
|
||||
const nonRentalPeriods = unavailablePeriods.filter(p => !p.isRentalSelection);
|
||||
const newPeriod: UnavailablePeriod = {
|
||||
id: Date.now().toString(),
|
||||
startDate: start,
|
||||
endDate: end,
|
||||
// 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
|
||||
};
|
||||
onPeriodsChange([...nonRentalPeriods, newPeriod]);
|
||||
|
||||
// Reset selection
|
||||
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 toggleDateAvailability = (date: Date) => {
|
||||
const dateStr = date.toISOString().split('T')[0];
|
||||
|
||||
if (isRentalMode) {
|
||||
// In rental mode, only handle rental selections (green), not unavailable periods (red)
|
||||
const existingRentalPeriod = unavailablePeriods.find(period => {
|
||||
const periodStart = new Date(period.startDate).toISOString().split('T')[0];
|
||||
const periodEnd = new Date(period.endDate).toISOString().split('T')[0];
|
||||
return period.isRentalSelection && periodStart === dateStr && periodEnd === dateStr && !period.startTime && !period.endTime;
|
||||
});
|
||||
|
||||
// Check if date is already unavailable (not a rental selection)
|
||||
const isUnavailable = unavailablePeriods.some(p =>
|
||||
!p.isRentalSelection && isDateInPeriod(date, p)
|
||||
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 (isUnavailable) {
|
||||
// Can't select unavailable dates
|
||||
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;
|
||||
}
|
||||
|
||||
if (existingRentalPeriod) {
|
||||
// Remove the rental selection
|
||||
onPeriodsChange(unavailablePeriods.filter(p => p.id !== existingRentalPeriod.id));
|
||||
} else {
|
||||
// Add new rental selection
|
||||
const newPeriod: UnavailablePeriod = {
|
||||
id: Date.now().toString(),
|
||||
startDate: date,
|
||||
endDate: date,
|
||||
isRentalSelection: true
|
||||
};
|
||||
onPeriodsChange([...unavailablePeriods, newPeriod]);
|
||||
}
|
||||
} else {
|
||||
// Original behavior for marking unavailable
|
||||
const existingPeriod = unavailablePeriods.find(period => {
|
||||
const periodStart = new Date(period.startDate).toISOString().split('T')[0];
|
||||
const periodEnd = new Date(period.endDate).toISOString().split('T')[0];
|
||||
return periodStart === dateStr && periodEnd === dateStr && !period.startTime && !period.endTime;
|
||||
});
|
||||
|
||||
if (existingPeriod) {
|
||||
// Remove the period to make it available
|
||||
onPeriodsChange(unavailablePeriods.filter(p => p.id !== existingPeriod.id));
|
||||
} else {
|
||||
// Add new unavailable period for this date
|
||||
const newPeriod: UnavailablePeriod = {
|
||||
id: Date.now().toString(),
|
||||
startDate: date,
|
||||
endDate: date
|
||||
};
|
||||
onPeriodsChange([...unavailablePeriods, newPeriod]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const toggleHourAvailability = (date: Date, hour: number) => {
|
||||
const startTime = `${hour.toString().padStart(2, '0')}:00`;
|
||||
const endTime = `${hour.toString().padStart(2, '0')}:59`;
|
||||
|
||||
const existingPeriod = unavailablePeriods.find(period => {
|
||||
const periodDate = new Date(period.startDate).toISOString().split('T')[0];
|
||||
const checkDate = date.toISOString().split('T')[0];
|
||||
return periodDate === checkDate &&
|
||||
period.startTime === startTime &&
|
||||
period.endTime === endTime;
|
||||
});
|
||||
|
||||
if (existingPeriod) {
|
||||
// Remove the period to make it available
|
||||
onPeriodsChange(unavailablePeriods.filter(p => p.id !== existingPeriod.id));
|
||||
} else {
|
||||
// Add new unavailable period for this hour
|
||||
// Create unavailable period
|
||||
const newPeriod: UnavailablePeriod = {
|
||||
id: Date.now().toString(),
|
||||
startDate: date,
|
||||
endDate: date,
|
||||
startTime: startTime,
|
||||
endTime: endTime
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -341,45 +590,66 @@ const AvailabilityCalendar: React.FC<AvailabilityCalendarProps> = ({
|
||||
if (date) {
|
||||
className += ' border cursor-pointer';
|
||||
|
||||
const rentalPeriod = unavailablePeriods.find(p =>
|
||||
p.isRentalSelection && isDateInPeriod(date, p)
|
||||
);
|
||||
|
||||
const acceptedRental = unavailablePeriods.find(p =>
|
||||
p.isAcceptedRental && isDateInPeriod(date, p)
|
||||
);
|
||||
|
||||
// Check if date is the selection start
|
||||
const isSelectionStart = selectionStart && date.getTime() === selectionStart.getTime();
|
||||
|
||||
// Check if date would be in the range if this was the end date
|
||||
const wouldBeInRange = selectionStart && date >=
|
||||
new Date(Math.min(selectionStart.getTime(), date.getTime())) &&
|
||||
date <= new Date(Math.max(selectionStart.getTime(), date.getTime()));
|
||||
|
||||
if (acceptedRental) {
|
||||
className += ' text-white';
|
||||
title = 'Booked - This date has an accepted rental';
|
||||
backgroundColor = '#6f42c1';
|
||||
} else if (rentalPeriod) {
|
||||
className += ' bg-success text-white';
|
||||
title = isRentalMode ? 'Selected for rental - Click to clear' : 'Selected';
|
||||
} else if (isSelectionStart) {
|
||||
className += ' bg-primary text-white';
|
||||
title = 'Start date selected - Click another date to complete selection';
|
||||
} else if (wouldBeInRange && !isDateFullyUnavailable(date) && !isDatePartiallyUnavailable(date)) {
|
||||
className += ' bg-info bg-opacity-25';
|
||||
title = 'Click to set as end date';
|
||||
} else if (isDateFullyUnavailable(date)) {
|
||||
className += ' bg-danger text-white';
|
||||
title = isRentalMode ? 'Unavailable' : 'Fully unavailable - Click to make available';
|
||||
} else if (isDatePartiallyUnavailable(date)) {
|
||||
className += ' text-dark';
|
||||
title = isRentalMode ? 'Partially unavailable' : 'Partially unavailable - Click to view details';
|
||||
backgroundColor = '#ffeb3b';
|
||||
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 {
|
||||
className += ' bg-light';
|
||||
title = isRentalMode ? 'Available - Click to select' : 'Available - Click to make unavailable';
|
||||
// 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';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -393,7 +663,7 @@ const AvailabilityCalendar: React.FC<AvailabilityCalendarProps> = ({
|
||||
cursor: date ? 'pointer' : 'default',
|
||||
backgroundColor: backgroundColor
|
||||
}}
|
||||
title={date ? title : ''}
|
||||
title={mode === 'owner' && date ? title : ''}
|
||||
>
|
||||
{date?.getDate()}
|
||||
</div>
|
||||
@@ -428,22 +698,93 @@ const AvailabilityCalendar: React.FC<AvailabilityCalendarProps> = ({
|
||||
<tbody>
|
||||
{hours.map(hour => (
|
||||
<tr key={hour}>
|
||||
<td className="text-center small">
|
||||
{hour.toString().padStart(2, '0')}:00
|
||||
<td className="text-center small" style={{ paddingBottom: '12px' }}>
|
||||
{hour === 0 ? '12 AM' : hour === 12 ? '12 PM' : hour < 12 ? `${hour} AM` : `${hour - 12} PM`}
|
||||
</td>
|
||||
{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 (
|
||||
<td
|
||||
key={dayIndex}
|
||||
className={`text-center cursor-pointer p-1
|
||||
${isUnavailable ? 'bg-danger text-white' : 'bg-light'}`}
|
||||
onClick={() => toggleHourAvailability(date, hour)}
|
||||
style={{ cursor: 'pointer', height: '30px' }}
|
||||
title={isUnavailable ? 'Click to make available' : 'Click to make unavailable'}
|
||||
className={cellClass}
|
||||
onClick={() => {
|
||||
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 && '×'}
|
||||
{isUnavailable && <span style={{ fontSize: '1.2rem' }}>×</span>}
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
@@ -473,18 +814,85 @@ const AvailabilityCalendar: React.FC<AvailabilityCalendarProps> = ({
|
||||
<tbody>
|
||||
{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 (
|
||||
<tr key={hour}>
|
||||
<td className="text-center" style={{ width: '100px' }}>
|
||||
{hour.toString().padStart(2, '0')}:00
|
||||
{hour === 0 ? '12:00 AM' : hour === 12 ? '12:00 PM' : hour < 12 ? `${hour}:00 AM` : `${hour - 12}:00 PM`}
|
||||
</td>
|
||||
<td
|
||||
className={`text-center cursor-pointer p-3
|
||||
${isUnavailable ? 'bg-danger text-white' : 'bg-light'}`}
|
||||
onClick={() => toggleHourAvailability(currentDate, hour)}
|
||||
style={{ cursor: 'pointer' }}
|
||||
title={isUnavailable ? 'Click to make available' : 'Click to make unavailable'}
|
||||
className={cellClass}
|
||||
onClick={() => {
|
||||
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'}
|
||||
</td>
|
||||
@@ -527,24 +935,20 @@ const AvailabilityCalendar: React.FC<AvailabilityCalendarProps> = ({
|
||||
>
|
||||
Month
|
||||
</button>
|
||||
{priceType === 'hour' && (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
className={`btn btn-sm ${viewType === 'week' ? 'btn-primary' : 'btn-outline-primary'}`}
|
||||
onClick={() => setViewType('week')}
|
||||
>
|
||||
Week
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`btn btn-sm ${viewType === 'day' ? 'btn-primary' : 'btn-outline-primary'}`}
|
||||
onClick={() => setViewType('day')}
|
||||
>
|
||||
Day
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className={`btn btn-sm ${viewType === 'week' ? 'btn-primary' : 'btn-outline-primary'}`}
|
||||
onClick={() => setViewType('week')}
|
||||
>
|
||||
Week
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`btn btn-sm ${viewType === 'day' ? 'btn-primary' : 'btn-outline-primary'}`}
|
||||
onClick={() => setViewType('day')}
|
||||
>
|
||||
Day
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@@ -572,22 +976,22 @@ const AvailabilityCalendar: React.FC<AvailabilityCalendarProps> = ({
|
||||
</div>
|
||||
|
||||
<div className="text-muted small">
|
||||
<div className="mb-2">
|
||||
<i className="bi bi-info-circle"></i> {isRentalMode ? 'Click start date, then click end date to select rental period' : 'Click on any date or time slot to toggle availability'}
|
||||
</div>
|
||||
{viewType === 'month' && (
|
||||
<div className="d-flex gap-3 justify-content-center flex-wrap">
|
||||
<span><span className="badge bg-light text-dark">□</span> Available</span>
|
||||
{isRentalMode && (
|
||||
<span><span className="badge bg-success">□</span> Selected</span>
|
||||
)}
|
||||
{!isRentalMode && (
|
||||
<span><span className="badge text-white" style={{ backgroundColor: '#6f42c1' }}>□</span> Booked</span>
|
||||
)}
|
||||
<div className="d-flex gap-3 justify-content-center flex-wrap">
|
||||
<span><span className="badge bg-light text-dark">□</span> Available</span>
|
||||
{mode === 'renter' && selectedRentalPeriod && (
|
||||
<span><span className="badge bg-success">□</span> Your Selection</span>
|
||||
)}
|
||||
{mode === 'owner' && ownerSelectionStart && (
|
||||
<span><span className="badge bg-warning text-dark">□</span> Selection Start</span>
|
||||
)}
|
||||
{mode === 'owner' && (
|
||||
<span><span className="badge text-white" style={{ backgroundColor: '#6f42c1' }}>□</span> Booked Rental</span>
|
||||
)}
|
||||
{viewType === 'month' && (
|
||||
<span><span className="badge text-dark" style={{ backgroundColor: '#ffeb3b' }}>□</span> Partially Unavailable</span>
|
||||
<span><span className="badge bg-danger">□</span> Fully Unavailable</span>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
<span><span className="badge bg-danger">□</span> {mode === 'owner' ? 'Marked Unavailable' : 'Not Available'}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { Navigate } from 'react-router-dom';
|
||||
import { Navigate, useLocation } from 'react-router-dom';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
|
||||
interface PrivateRouteProps {
|
||||
@@ -8,6 +8,7 @@ interface PrivateRouteProps {
|
||||
|
||||
const PrivateRoute: React.FC<PrivateRouteProps> = ({ children }) => {
|
||||
const { user, loading } = useAuth();
|
||||
const location = useLocation();
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
@@ -19,7 +20,7 @@ const PrivateRoute: React.FC<PrivateRouteProps> = ({ children }) => {
|
||||
);
|
||||
}
|
||||
|
||||
return user ? <>{children}</> : <Navigate to="/login" />;
|
||||
return user ? <>{children}</> : <Navigate to="/login" state={{ from: location }} replace />;
|
||||
};
|
||||
|
||||
export default PrivateRoute;
|
||||
@@ -435,7 +435,7 @@ const CreateItem: React.FC = () => {
|
||||
onPeriodsChange={(periods) =>
|
||||
setFormData(prev => ({ ...prev, unavailablePeriods: periods }))
|
||||
}
|
||||
priceType={priceType}
|
||||
mode="owner"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -546,7 +546,7 @@ const EditItem: React.FC = () => {
|
||||
const userPeriods = periods.filter(p => !p.isAcceptedRental);
|
||||
setFormData(prev => ({ ...prev, unavailablePeriods: userPeriods }));
|
||||
}}
|
||||
priceType={priceType}
|
||||
mode="owner"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -18,6 +18,9 @@ const ItemDetail: React.FC = () => {
|
||||
|
||||
useEffect(() => {
|
||||
fetchItem();
|
||||
}, [id]);
|
||||
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
checkIfAlreadyRenting();
|
||||
}
|
||||
@@ -207,7 +210,7 @@ const ItemDetail: React.FC = () => {
|
||||
|
||||
<ItemReviews itemId={item.id} />
|
||||
|
||||
<div className="d-flex gap-2">
|
||||
<div className="d-flex gap-2 mb-5">
|
||||
{isOwner ? (
|
||||
<button className="btn btn-primary" onClick={handleEdit}>
|
||||
Edit Listing
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { Link, useNavigate, useLocation } from 'react-router-dom';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
|
||||
const Login: React.FC = () => {
|
||||
@@ -9,6 +9,9 @@ const Login: React.FC = () => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { login } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
const from = location.state?.from?.pathname || '/';
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
@@ -17,7 +20,7 @@ const Login: React.FC = () => {
|
||||
|
||||
try {
|
||||
await login(email, password);
|
||||
navigate('/');
|
||||
navigate(from, { replace: true });
|
||||
} catch (err: any) {
|
||||
setError(err.response?.data?.error || 'Failed to login');
|
||||
} finally {
|
||||
@@ -75,7 +78,7 @@ const Login: React.FC = () => {
|
||||
<div className="text-center mt-3">
|
||||
<p className="mb-0">
|
||||
Don't have an account?{' '}
|
||||
<Link to="/register" className="text-decoration-none">
|
||||
<Link to="/register" state={{ from: location.state?.from }} className="text-decoration-none">
|
||||
Sign up
|
||||
</Link>
|
||||
</p>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { Link, useNavigate, useLocation } from 'react-router-dom';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
|
||||
const Register: React.FC = () => {
|
||||
@@ -15,6 +15,9 @@ const Register: React.FC = () => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { register } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
const from = location.state?.from?.pathname || '/';
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setFormData({
|
||||
@@ -30,7 +33,7 @@ const Register: React.FC = () => {
|
||||
|
||||
try {
|
||||
await register(formData);
|
||||
navigate('/');
|
||||
navigate(from, { replace: true });
|
||||
} catch (err: any) {
|
||||
setError(err.response?.data?.error || 'Failed to create account');
|
||||
} finally {
|
||||
|
||||
@@ -22,6 +22,13 @@ const RentItem: React.FC = () => {
|
||||
cardCVC: '',
|
||||
cardName: ''
|
||||
});
|
||||
|
||||
const [manualSelection, setManualSelection] = useState({
|
||||
startDate: '',
|
||||
startTime: '09:00',
|
||||
endDate: '',
|
||||
endTime: '17:00'
|
||||
});
|
||||
|
||||
const [selectedPeriods, setSelectedPeriods] = useState<Array<{
|
||||
id: string;
|
||||
@@ -41,6 +48,48 @@ const RentItem: React.FC = () => {
|
||||
useEffect(() => {
|
||||
calculateTotal();
|
||||
}, [selectedPeriods, item]);
|
||||
|
||||
useEffect(() => {
|
||||
// Sync manual selection with selected periods
|
||||
if (selectedPeriods.length > 0) {
|
||||
const period = selectedPeriods[0];
|
||||
// Extract hours from the Date objects if startTime/endTime not provided
|
||||
let startTimeStr = period.startTime;
|
||||
let endTimeStr = period.endTime;
|
||||
|
||||
if (!startTimeStr) {
|
||||
const startHour = period.startDate.getHours();
|
||||
startTimeStr = `${startHour.toString().padStart(2, '0')}:00`;
|
||||
}
|
||||
|
||||
if (!endTimeStr) {
|
||||
const endHour = period.endDate.getHours();
|
||||
// If the end hour is 23:59:59, show it as 00:00 of the next day
|
||||
if (endHour === 23 && period.endDate.getMinutes() === 59) {
|
||||
endTimeStr = '00:00';
|
||||
// Adjust the end date to show the next day
|
||||
const adjustedEndDate = new Date(period.endDate);
|
||||
adjustedEndDate.setDate(adjustedEndDate.getDate() + 1);
|
||||
setManualSelection({
|
||||
startDate: period.startDate.toISOString().split('T')[0],
|
||||
startTime: startTimeStr,
|
||||
endDate: adjustedEndDate.toISOString().split('T')[0],
|
||||
endTime: endTimeStr
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
endTimeStr = `${endHour.toString().padStart(2, '0')}:00`;
|
||||
}
|
||||
}
|
||||
|
||||
setManualSelection({
|
||||
startDate: period.startDate.toISOString().split('T')[0],
|
||||
startTime: startTimeStr,
|
||||
endDate: period.endDate.toISOString().split('T')[0],
|
||||
endTime: endTimeStr
|
||||
});
|
||||
}
|
||||
}, [selectedPeriods]);
|
||||
|
||||
const fetchItem = async () => {
|
||||
try {
|
||||
@@ -176,6 +225,59 @@ const RentItem: React.FC = () => {
|
||||
setFormData(prev => ({ ...prev, [name]: value }));
|
||||
}
|
||||
};
|
||||
|
||||
const handleManualSelectionChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
|
||||
const updatedSelection = {
|
||||
...manualSelection,
|
||||
[e.target.name]: e.target.value
|
||||
};
|
||||
setManualSelection(updatedSelection);
|
||||
|
||||
// Automatically apply selection if both dates are set
|
||||
if (updatedSelection.startDate && updatedSelection.endDate) {
|
||||
// Create dates with time set to midnight to avoid timezone issues
|
||||
const start = new Date(updatedSelection.startDate + 'T00:00:00');
|
||||
const end = new Date(updatedSelection.endDate + 'T00:00:00');
|
||||
|
||||
// Add time for both hourly and daily rentals
|
||||
const [startHour, startMin] = updatedSelection.startTime.split(':').map(Number);
|
||||
const [endHour, endMin] = updatedSelection.endTime.split(':').map(Number);
|
||||
start.setHours(startHour, startMin, 0, 0);
|
||||
end.setHours(endHour, endMin, 0, 0);
|
||||
|
||||
// Note: We keep the times as selected by the user
|
||||
// The calendar will interpret 00:00 correctly
|
||||
|
||||
// Validate dates
|
||||
if (end < start) {
|
||||
setError('End date/time must be after start date/time');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if dates are available
|
||||
const unavailable = item?.unavailablePeriods?.some(period => {
|
||||
const periodStart = new Date(period.startDate);
|
||||
const periodEnd = new Date(period.endDate);
|
||||
return (start >= periodStart && start <= periodEnd) ||
|
||||
(end >= periodStart && end <= periodEnd) ||
|
||||
(start <= periodStart && end >= periodEnd);
|
||||
});
|
||||
|
||||
if (unavailable) {
|
||||
setError('Selected dates include unavailable periods');
|
||||
return;
|
||||
}
|
||||
|
||||
setError(null);
|
||||
setSelectedPeriods([{
|
||||
id: Date.now().toString(),
|
||||
startDate: start,
|
||||
endDate: end,
|
||||
startTime: updatedSelection.startTime,
|
||||
endTime: updatedSelection.endTime
|
||||
}]);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
@@ -223,42 +325,117 @@ const RentItem: React.FC = () => {
|
||||
<h5 className="card-title">Select Rental Period</h5>
|
||||
|
||||
<AvailabilityCalendar
|
||||
unavailablePeriods={[
|
||||
...(item.unavailablePeriods || []),
|
||||
...selectedPeriods.map(p => ({ ...p, isRentalSelection: true }))
|
||||
]}
|
||||
onPeriodsChange={(periods) => {
|
||||
// Only handle rental selections
|
||||
const rentalSelections = periods.filter(p => p.isRentalSelection);
|
||||
setSelectedPeriods(rentalSelections.map(p => {
|
||||
const { isRentalSelection, ...rest } = p;
|
||||
return rest;
|
||||
}));
|
||||
unavailablePeriods={item.unavailablePeriods || []}
|
||||
onPeriodsChange={() => {}} // Read-only for renters
|
||||
mode="renter"
|
||||
selectedRentalPeriod={selectedPeriods.length > 0 ? {
|
||||
id: selectedPeriods[0].id,
|
||||
startDate: selectedPeriods[0].startDate,
|
||||
endDate: selectedPeriods[0].endDate,
|
||||
startTime: selectedPeriods[0].startTime,
|
||||
endTime: selectedPeriods[0].endTime,
|
||||
isRentalSelection: true
|
||||
} : undefined}
|
||||
onRentalSelect={(period) => {
|
||||
// Update selected periods
|
||||
setSelectedPeriods([{
|
||||
id: Date.now().toString(),
|
||||
startDate: period.startDate,
|
||||
endDate: period.endDate,
|
||||
startTime: period.startTime,
|
||||
endTime: period.endTime
|
||||
}]);
|
||||
|
||||
// Update manual selection to match calendar
|
||||
setManualSelection({
|
||||
startDate: period.startDate.toISOString().split('T')[0],
|
||||
startTime: period.startTime || '09:00',
|
||||
endDate: period.endDate.toISOString().split('T')[0],
|
||||
endTime: period.endTime || '17:00'
|
||||
});
|
||||
}}
|
||||
priceType={showHourlyOptions ? "hour" : "day"}
|
||||
isRentalMode={true}
|
||||
/>
|
||||
|
||||
<div className="mt-4">
|
||||
<div className="row g-3">
|
||||
<div className="col-md-3">
|
||||
<label htmlFor="startDate" className="form-label">Start Date</label>
|
||||
<input
|
||||
type="date"
|
||||
className="form-control"
|
||||
id="startDate"
|
||||
name="startDate"
|
||||
value={manualSelection.startDate}
|
||||
onChange={handleManualSelectionChange}
|
||||
min={new Date().toISOString().split('T')[0]}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-md-3">
|
||||
<label htmlFor="startTime" className="form-label">Start Time</label>
|
||||
<select
|
||||
className="form-select"
|
||||
id="startTime"
|
||||
name="startTime"
|
||||
value={manualSelection.startTime}
|
||||
onChange={handleManualSelectionChange}
|
||||
>
|
||||
{Array.from({ length: 24 }, (_, i) => {
|
||||
const hour = i === 0 ? 12 : i > 12 ? i - 12 : i;
|
||||
const period = i < 12 ? 'AM' : 'PM';
|
||||
return (
|
||||
<option key={i} value={`${i.toString().padStart(2, '0')}:00`}>
|
||||
{hour}:00 {period}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
</div>
|
||||
<div className="col-md-3">
|
||||
<label htmlFor="endDate" className="form-label">End Date</label>
|
||||
<input
|
||||
type="date"
|
||||
className="form-control"
|
||||
id="endDate"
|
||||
name="endDate"
|
||||
value={manualSelection.endDate}
|
||||
onChange={handleManualSelectionChange}
|
||||
min={manualSelection.startDate || new Date().toISOString().split('T')[0]}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-md-3">
|
||||
<label htmlFor="endTime" className="form-label">End Time</label>
|
||||
<select
|
||||
className="form-select"
|
||||
id="endTime"
|
||||
name="endTime"
|
||||
value={manualSelection.endTime}
|
||||
onChange={handleManualSelectionChange}
|
||||
>
|
||||
{Array.from({ length: 24 }, (_, i) => {
|
||||
const hour = i === 0 ? 12 : i > 12 ? i - 12 : i;
|
||||
const period = i < 12 ? 'AM' : 'PM';
|
||||
return (
|
||||
<option key={i} value={`${i.toString().padStart(2, '0')}:00`}>
|
||||
{hour}:00 {period}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{rentalDuration.days < minDays && rentalDuration.days > 0 && !showHourlyOptions && (
|
||||
<div className="alert alert-warning mt-3">
|
||||
Minimum rental period is {minDays} days
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedPeriods.length === 0 && (
|
||||
<div className="alert alert-info mt-3">
|
||||
Please select your rental dates on the calendar above
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card mb-4">
|
||||
<div className="card-body">
|
||||
<h5 className="card-title">Delivery Options</h5>
|
||||
|
||||
<div className="mb-3">
|
||||
<label htmlFor="deliveryMethod" className="form-label">Delivery Method *</label>
|
||||
<div className="my-3">
|
||||
<select
|
||||
className="form-select"
|
||||
id="deliveryMethod"
|
||||
|
||||
@@ -21,8 +21,16 @@ api.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error) => {
|
||||
if (error.response?.status === 401) {
|
||||
localStorage.removeItem('token');
|
||||
window.location.href = '/login';
|
||||
// Only redirect to login if we have a token (user was logged in)
|
||||
const token = localStorage.getItem('token');
|
||||
|
||||
if (token) {
|
||||
// User was logged in but token expired/invalid
|
||||
localStorage.removeItem('token');
|
||||
window.location.href = '/login';
|
||||
}
|
||||
// For non-authenticated users, just reject the error without redirecting
|
||||
// Let individual components handle 401 errors as needed
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user