date selection
This commit is contained in:
@@ -13,8 +13,9 @@ interface UnavailablePeriod {
|
|||||||
interface AvailabilityCalendarProps {
|
interface AvailabilityCalendarProps {
|
||||||
unavailablePeriods: UnavailablePeriod[];
|
unavailablePeriods: UnavailablePeriod[];
|
||||||
onPeriodsChange: (periods: UnavailablePeriod[]) => void;
|
onPeriodsChange: (periods: UnavailablePeriod[]) => void;
|
||||||
priceType?: 'hour' | 'day';
|
mode: 'owner' | 'renter'; // Clear mode distinction
|
||||||
isRentalMode?: boolean;
|
onRentalSelect?: (period: { startDate: Date; endDate: Date; startTime?: string; endTime?: string }) => void;
|
||||||
|
selectedRentalPeriod?: UnavailablePeriod; // External selection to display
|
||||||
}
|
}
|
||||||
|
|
||||||
type ViewType = 'month' | 'week' | 'day';
|
type ViewType = 'month' | 'week' | 'day';
|
||||||
@@ -22,19 +23,20 @@ type ViewType = 'month' | 'week' | 'day';
|
|||||||
const AvailabilityCalendar: React.FC<AvailabilityCalendarProps> = ({
|
const AvailabilityCalendar: React.FC<AvailabilityCalendarProps> = ({
|
||||||
unavailablePeriods,
|
unavailablePeriods,
|
||||||
onPeriodsChange,
|
onPeriodsChange,
|
||||||
priceType = 'hour',
|
mode,
|
||||||
isRentalMode = false
|
onRentalSelect,
|
||||||
|
selectedRentalPeriod: externalSelectedPeriod
|
||||||
}) => {
|
}) => {
|
||||||
const [currentDate, setCurrentDate] = useState(new Date());
|
const [currentDate, setCurrentDate] = useState(new Date());
|
||||||
const [viewType, setViewType] = useState<ViewType>('month');
|
const [viewType, setViewType] = useState<ViewType>('month');
|
||||||
const [selectionStart, setSelectionStart] = useState<Date | null>(null);
|
const [selectionStart, setSelectionStart] = useState<Date | null>(null);
|
||||||
|
const [selectionStartHour, setSelectionStartHour] = useState<number | null>(null);
|
||||||
// Reset to month view if priceType is day and current view is week/day
|
const [internalSelectedPeriod, setInternalSelectedPeriod] = useState<UnavailablePeriod | null>(null);
|
||||||
useEffect(() => {
|
const [ownerSelectionStart, setOwnerSelectionStart] = useState<Date | null>(null);
|
||||||
if (priceType === 'day' && (viewType === 'week' || viewType === 'day')) {
|
const [ownerSelectionStartHour, setOwnerSelectionStartHour] = useState<number | null>(null);
|
||||||
setViewType('month');
|
|
||||||
}
|
// Use external selection if provided, otherwise use internal
|
||||||
}, [priceType]);
|
const selectedRentalPeriod = externalSelectedPeriod || internalSelectedPeriod;
|
||||||
|
|
||||||
const getDaysInMonth = (date: Date) => {
|
const getDaysInMonth = (date: Date) => {
|
||||||
const year = date.getFullYear();
|
const year = date.getFullYear();
|
||||||
@@ -77,10 +79,14 @@ const AvailabilityCalendar: React.FC<AvailabilityCalendarProps> = ({
|
|||||||
const isDateInPeriod = (date: Date, period: UnavailablePeriod) => {
|
const isDateInPeriod = (date: Date, period: UnavailablePeriod) => {
|
||||||
const start = new Date(period.startDate);
|
const start = new Date(period.startDate);
|
||||||
const end = new Date(period.endDate);
|
const end = new Date(period.endDate);
|
||||||
start.setHours(0, 0, 0, 0);
|
|
||||||
end.setHours(23, 59, 59, 999);
|
|
||||||
const checkDate = new Date(date);
|
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);
|
checkDate.setHours(0, 0, 0, 0);
|
||||||
|
|
||||||
|
// Basic date range check
|
||||||
return checkDate >= start && checkDate <= end;
|
return checkDate >= start && checkDate <= end;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -158,157 +164,400 @@ const AvailabilityCalendar: React.FC<AvailabilityCalendarProps> = ({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDateClick = (date: Date) => {
|
const handleHourClick = (date: Date, hour: number) => {
|
||||||
// Check if this date has an accepted rental
|
if (mode !== 'renter') return;
|
||||||
const hasAcceptedRental = unavailablePeriods.some(p =>
|
|
||||||
p.isAcceptedRental && isDateInPeriod(date, p)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (hasAcceptedRental) {
|
// Check if this hour is unavailable
|
||||||
// Don't allow clicking on accepted rental dates
|
if (isHourUnavailable(date, hour)) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isRentalMode) {
|
if (!selectionStart || !selectionStartHour) {
|
||||||
toggleDateAvailability(date);
|
// First click - set start date and hour
|
||||||
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
|
|
||||||
setSelectionStart(date);
|
setSelectionStart(date);
|
||||||
|
setSelectionStartHour(hour);
|
||||||
|
setInternalSelectedPeriod(null);
|
||||||
} else {
|
} else {
|
||||||
// Second click - create rental period
|
// Second click - complete selection
|
||||||
const start = new Date(Math.min(selectionStart.getTime(), date.getTime()));
|
const startDate = new Date(selectionStart);
|
||||||
const end = new Date(Math.max(selectionStart.getTime(), date.getTime()));
|
const endDate = new Date(date);
|
||||||
|
|
||||||
// Check if any date in range is unavailable
|
// Determine which date/hour comes first
|
||||||
let currentDate = new Date(start);
|
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;
|
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;
|
hasUnavailable = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
currentDate.setDate(currentDate.getDate() + 1);
|
current.setHours(current.getHours() + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasUnavailable) {
|
if (hasUnavailable) {
|
||||||
// Range contains unavailable dates, reset selection
|
// Range contains unavailable hours, reset
|
||||||
setSelectionStart(null);
|
setSelectionStart(null);
|
||||||
|
setSelectionStartHour(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear existing rental selections and add new one
|
// Create rental selection
|
||||||
const nonRentalPeriods = unavailablePeriods.filter(p => !p.isRentalSelection);
|
const rentalPeriod: UnavailablePeriod = {
|
||||||
const newPeriod: UnavailablePeriod = {
|
id: 'rental-selection',
|
||||||
id: Date.now().toString(),
|
startDate: finalStart,
|
||||||
startDate: start,
|
endDate: finalEnd,
|
||||||
endDate: end,
|
startTime: `${startHour.toString().padStart(2, '0')}:00`,
|
||||||
|
endTime: `${endHour.toString().padStart(2, '0')}:00`,
|
||||||
isRentalSelection: true
|
isRentalSelection: true
|
||||||
};
|
};
|
||||||
onPeriodsChange([...nonRentalPeriods, newPeriod]);
|
|
||||||
|
|
||||||
// Reset selection
|
setInternalSelectedPeriod(rentalPeriod);
|
||||||
setSelectionStart(null);
|
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 handleDateClick = (date: Date) => {
|
||||||
const dateStr = date.toISOString().split('T')[0];
|
if (mode === 'owner') {
|
||||||
|
// OWNER MODE: Toggle unavailability for dates
|
||||||
if (isRentalMode) {
|
// Check if this date has an accepted rental - can't modify those
|
||||||
// In rental mode, only handle rental selections (green), not unavailable periods (red)
|
const hasAcceptedRental = unavailablePeriods.some(p =>
|
||||||
const existingRentalPeriod = unavailablePeriods.find(period => {
|
p.isAcceptedRental && isDateInPeriod(date, p)
|
||||||
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)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isUnavailable) {
|
if (hasAcceptedRental) {
|
||||||
// Can't select unavailable dates
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (existingRentalPeriod) {
|
// Create unavailable period
|
||||||
// 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
|
|
||||||
const newPeriod: UnavailablePeriod = {
|
const newPeriod: UnavailablePeriod = {
|
||||||
id: Date.now().toString(),
|
id: Date.now().toString(),
|
||||||
startDate: date,
|
startDate: finalStart,
|
||||||
endDate: date,
|
endDate: finalEnd,
|
||||||
startTime: startTime,
|
startTime: `${startHour.toString().padStart(2, '0')}:00`,
|
||||||
endTime: endTime
|
endTime: `${endHour.toString().padStart(2, '0')}:59`
|
||||||
};
|
};
|
||||||
|
|
||||||
onPeriodsChange([...unavailablePeriods, newPeriod]);
|
onPeriodsChange([...unavailablePeriods, newPeriod]);
|
||||||
|
setOwnerSelectionStart(null);
|
||||||
|
setOwnerSelectionStartHour(null);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -341,45 +590,66 @@ const AvailabilityCalendar: React.FC<AvailabilityCalendarProps> = ({
|
|||||||
if (date) {
|
if (date) {
|
||||||
className += ' border cursor-pointer';
|
className += ' border cursor-pointer';
|
||||||
|
|
||||||
const rentalPeriod = unavailablePeriods.find(p =>
|
if (mode === 'owner') {
|
||||||
p.isRentalSelection && isDateInPeriod(date, p)
|
// OWNER VIEW
|
||||||
);
|
const acceptedRental = unavailablePeriods.find(p =>
|
||||||
|
p.isAcceptedRental && isDateInPeriod(date, p)
|
||||||
const acceptedRental = unavailablePeriods.find(p =>
|
);
|
||||||
p.isAcceptedRental && isDateInPeriod(date, p)
|
const isOwnerStart = ownerSelectionStart && date.getTime() === ownerSelectionStart.getTime();
|
||||||
);
|
|
||||||
|
if (acceptedRental) {
|
||||||
// Check if date is the selection start
|
className += ' text-white';
|
||||||
const isSelectionStart = selectionStart && date.getTime() === selectionStart.getTime();
|
title = 'Booked - This date has an accepted rental';
|
||||||
|
backgroundColor = '#6f42c1';
|
||||||
// Check if date would be in the range if this was the end date
|
} else if (isOwnerStart) {
|
||||||
const wouldBeInRange = selectionStart && date >=
|
className += ' bg-warning text-dark';
|
||||||
new Date(Math.min(selectionStart.getTime(), date.getTime())) &&
|
title = 'Click another date to mark range as unavailable';
|
||||||
date <= new Date(Math.max(selectionStart.getTime(), date.getTime()));
|
} else if (isDateFullyUnavailable(date)) {
|
||||||
|
className += ' bg-danger text-white';
|
||||||
if (acceptedRental) {
|
title = 'Unavailable - Click to make available';
|
||||||
className += ' text-white';
|
} else if (isDatePartiallyUnavailable(date)) {
|
||||||
title = 'Booked - This date has an accepted rental';
|
className += ' text-dark';
|
||||||
backgroundColor = '#6f42c1';
|
title = 'Partially unavailable - Switch to week/day view for details';
|
||||||
} else if (rentalPeriod) {
|
backgroundColor = '#ffeb3b';
|
||||||
className += ' bg-success text-white';
|
} else {
|
||||||
title = isRentalMode ? 'Selected for rental - Click to clear' : 'Selected';
|
className += ' bg-light';
|
||||||
} else if (isSelectionStart) {
|
title = ownerSelectionStart ? 'Click to complete selection' : 'Click to start selecting unavailable dates';
|
||||||
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';
|
|
||||||
} else {
|
} else {
|
||||||
className += ' bg-light';
|
// RENTER VIEW
|
||||||
title = isRentalMode ? 'Available - Click to select' : 'Available - Click to make unavailable';
|
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',
|
cursor: date ? 'pointer' : 'default',
|
||||||
backgroundColor: backgroundColor
|
backgroundColor: backgroundColor
|
||||||
}}
|
}}
|
||||||
title={date ? title : ''}
|
title={mode === 'owner' && date ? title : ''}
|
||||||
>
|
>
|
||||||
{date?.getDate()}
|
{date?.getDate()}
|
||||||
</div>
|
</div>
|
||||||
@@ -428,22 +698,93 @@ const AvailabilityCalendar: React.FC<AvailabilityCalendarProps> = ({
|
|||||||
<tbody>
|
<tbody>
|
||||||
{hours.map(hour => (
|
{hours.map(hour => (
|
||||||
<tr key={hour}>
|
<tr key={hour}>
|
||||||
<td className="text-center small">
|
<td className="text-center small" style={{ paddingBottom: '12px' }}>
|
||||||
{hour.toString().padStart(2, '0')}:00
|
{hour === 0 ? '12 AM' : hour === 12 ? '12 PM' : hour < 12 ? `${hour} AM` : `${hour - 12} PM`}
|
||||||
</td>
|
</td>
|
||||||
{days.map((date, dayIndex) => {
|
{days.map((date, dayIndex) => {
|
||||||
const isUnavailable = isHourUnavailable(date, hour);
|
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 (
|
return (
|
||||||
<td
|
<td
|
||||||
key={dayIndex}
|
key={dayIndex}
|
||||||
className={`text-center cursor-pointer p-1
|
className={cellClass}
|
||||||
${isUnavailable ? 'bg-danger text-white' : 'bg-light'}`}
|
onClick={() => {
|
||||||
onClick={() => toggleHourAvailability(date, hour)}
|
if (mode === 'owner') {
|
||||||
style={{ cursor: 'pointer', height: '30px' }}
|
toggleHourAvailability(date, hour);
|
||||||
title={isUnavailable ? 'Click to make available' : 'Click to make unavailable'}
|
} 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>
|
</td>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -473,18 +814,85 @@ const AvailabilityCalendar: React.FC<AvailabilityCalendarProps> = ({
|
|||||||
<tbody>
|
<tbody>
|
||||||
{hours.map(hour => {
|
{hours.map(hour => {
|
||||||
const isUnavailable = isHourUnavailable(currentDate, 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 (
|
return (
|
||||||
<tr key={hour}>
|
<tr key={hour}>
|
||||||
<td className="text-center" style={{ width: '100px' }}>
|
<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>
|
||||||
<td
|
<td
|
||||||
className={`text-center cursor-pointer p-3
|
className={cellClass}
|
||||||
${isUnavailable ? 'bg-danger text-white' : 'bg-light'}`}
|
onClick={() => {
|
||||||
onClick={() => toggleHourAvailability(currentDate, hour)}
|
if (mode === 'owner') {
|
||||||
style={{ cursor: 'pointer' }}
|
toggleHourAvailability(currentDate, hour);
|
||||||
title={isUnavailable ? 'Click to make available' : 'Click to make unavailable'}
|
} 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'}
|
{isUnavailable ? 'Unavailable' : 'Available'}
|
||||||
</td>
|
</td>
|
||||||
@@ -527,24 +935,20 @@ const AvailabilityCalendar: React.FC<AvailabilityCalendarProps> = ({
|
|||||||
>
|
>
|
||||||
Month
|
Month
|
||||||
</button>
|
</button>
|
||||||
{priceType === 'hour' && (
|
<button
|
||||||
<>
|
type="button"
|
||||||
<button
|
className={`btn btn-sm ${viewType === 'week' ? 'btn-primary' : 'btn-outline-primary'}`}
|
||||||
type="button"
|
onClick={() => setViewType('week')}
|
||||||
className={`btn btn-sm ${viewType === 'week' ? 'btn-primary' : 'btn-outline-primary'}`}
|
>
|
||||||
onClick={() => setViewType('week')}
|
Week
|
||||||
>
|
</button>
|
||||||
Week
|
<button
|
||||||
</button>
|
type="button"
|
||||||
<button
|
className={`btn btn-sm ${viewType === 'day' ? 'btn-primary' : 'btn-outline-primary'}`}
|
||||||
type="button"
|
onClick={() => setViewType('day')}
|
||||||
className={`btn btn-sm ${viewType === 'day' ? 'btn-primary' : 'btn-outline-primary'}`}
|
>
|
||||||
onClick={() => setViewType('day')}
|
Day
|
||||||
>
|
</button>
|
||||||
Day
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@@ -572,22 +976,22 @@ const AvailabilityCalendar: React.FC<AvailabilityCalendarProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-muted small">
|
<div className="text-muted small">
|
||||||
<div className="mb-2">
|
<div className="d-flex gap-3 justify-content-center flex-wrap">
|
||||||
<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'}
|
<span><span className="badge bg-light text-dark">□</span> Available</span>
|
||||||
</div>
|
{mode === 'renter' && selectedRentalPeriod && (
|
||||||
{viewType === 'month' && (
|
<span><span className="badge bg-success">□</span> Your Selection</span>
|
||||||
<div className="d-flex gap-3 justify-content-center flex-wrap">
|
)}
|
||||||
<span><span className="badge bg-light text-dark">□</span> Available</span>
|
{mode === 'owner' && ownerSelectionStart && (
|
||||||
{isRentalMode && (
|
<span><span className="badge bg-warning text-dark">□</span> Selection Start</span>
|
||||||
<span><span className="badge bg-success">□</span> Selected</span>
|
)}
|
||||||
)}
|
{mode === 'owner' && (
|
||||||
{!isRentalMode && (
|
<span><span className="badge text-white" style={{ backgroundColor: '#6f42c1' }}>□</span> Booked Rental</span>
|
||||||
<span><span className="badge text-white" style={{ backgroundColor: '#6f42c1' }}>□</span> Booked</span>
|
)}
|
||||||
)}
|
{viewType === 'month' && (
|
||||||
<span><span className="badge text-dark" style={{ backgroundColor: '#ffeb3b' }}>□</span> Partially Unavailable</span>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Navigate } from 'react-router-dom';
|
import { Navigate, useLocation } from 'react-router-dom';
|
||||||
import { useAuth } from '../contexts/AuthContext';
|
import { useAuth } from '../contexts/AuthContext';
|
||||||
|
|
||||||
interface PrivateRouteProps {
|
interface PrivateRouteProps {
|
||||||
@@ -8,6 +8,7 @@ interface PrivateRouteProps {
|
|||||||
|
|
||||||
const PrivateRoute: React.FC<PrivateRouteProps> = ({ children }) => {
|
const PrivateRoute: React.FC<PrivateRouteProps> = ({ children }) => {
|
||||||
const { user, loading } = useAuth();
|
const { user, loading } = useAuth();
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
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;
|
export default PrivateRoute;
|
||||||
@@ -435,7 +435,7 @@ const CreateItem: React.FC = () => {
|
|||||||
onPeriodsChange={(periods) =>
|
onPeriodsChange={(periods) =>
|
||||||
setFormData(prev => ({ ...prev, unavailablePeriods: periods }))
|
setFormData(prev => ({ ...prev, unavailablePeriods: periods }))
|
||||||
}
|
}
|
||||||
priceType={priceType}
|
mode="owner"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -546,7 +546,7 @@ const EditItem: React.FC = () => {
|
|||||||
const userPeriods = periods.filter(p => !p.isAcceptedRental);
|
const userPeriods = periods.filter(p => !p.isAcceptedRental);
|
||||||
setFormData(prev => ({ ...prev, unavailablePeriods: userPeriods }));
|
setFormData(prev => ({ ...prev, unavailablePeriods: userPeriods }));
|
||||||
}}
|
}}
|
||||||
priceType={priceType}
|
mode="owner"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,9 @@ const ItemDetail: React.FC = () => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchItem();
|
fetchItem();
|
||||||
|
}, [id]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
if (user) {
|
if (user) {
|
||||||
checkIfAlreadyRenting();
|
checkIfAlreadyRenting();
|
||||||
}
|
}
|
||||||
@@ -207,7 +210,7 @@ const ItemDetail: React.FC = () => {
|
|||||||
|
|
||||||
<ItemReviews itemId={item.id} />
|
<ItemReviews itemId={item.id} />
|
||||||
|
|
||||||
<div className="d-flex gap-2">
|
<div className="d-flex gap-2 mb-5">
|
||||||
{isOwner ? (
|
{isOwner ? (
|
||||||
<button className="btn btn-primary" onClick={handleEdit}>
|
<button className="btn btn-primary" onClick={handleEdit}>
|
||||||
Edit Listing
|
Edit Listing
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState } from 'react';
|
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';
|
import { useAuth } from '../contexts/AuthContext';
|
||||||
|
|
||||||
const Login: React.FC = () => {
|
const Login: React.FC = () => {
|
||||||
@@ -9,6 +9,9 @@ const Login: React.FC = () => {
|
|||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const { login } = useAuth();
|
const { login } = useAuth();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
const from = location.state?.from?.pathname || '/';
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -17,7 +20,7 @@ const Login: React.FC = () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await login(email, password);
|
await login(email, password);
|
||||||
navigate('/');
|
navigate(from, { replace: true });
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
setError(err.response?.data?.error || 'Failed to login');
|
setError(err.response?.data?.error || 'Failed to login');
|
||||||
} finally {
|
} finally {
|
||||||
@@ -75,7 +78,7 @@ const Login: React.FC = () => {
|
|||||||
<div className="text-center mt-3">
|
<div className="text-center mt-3">
|
||||||
<p className="mb-0">
|
<p className="mb-0">
|
||||||
Don't have an account?{' '}
|
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
|
Sign up
|
||||||
</Link>
|
</Link>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState } from 'react';
|
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';
|
import { useAuth } from '../contexts/AuthContext';
|
||||||
|
|
||||||
const Register: React.FC = () => {
|
const Register: React.FC = () => {
|
||||||
@@ -15,6 +15,9 @@ const Register: React.FC = () => {
|
|||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const { register } = useAuth();
|
const { register } = useAuth();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
const from = location.state?.from?.pathname || '/';
|
||||||
|
|
||||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
setFormData({
|
setFormData({
|
||||||
@@ -30,7 +33,7 @@ const Register: React.FC = () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await register(formData);
|
await register(formData);
|
||||||
navigate('/');
|
navigate(from, { replace: true });
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
setError(err.response?.data?.error || 'Failed to create account');
|
setError(err.response?.data?.error || 'Failed to create account');
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -22,6 +22,13 @@ const RentItem: React.FC = () => {
|
|||||||
cardCVC: '',
|
cardCVC: '',
|
||||||
cardName: ''
|
cardName: ''
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const [manualSelection, setManualSelection] = useState({
|
||||||
|
startDate: '',
|
||||||
|
startTime: '09:00',
|
||||||
|
endDate: '',
|
||||||
|
endTime: '17:00'
|
||||||
|
});
|
||||||
|
|
||||||
const [selectedPeriods, setSelectedPeriods] = useState<Array<{
|
const [selectedPeriods, setSelectedPeriods] = useState<Array<{
|
||||||
id: string;
|
id: string;
|
||||||
@@ -41,6 +48,48 @@ const RentItem: React.FC = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
calculateTotal();
|
calculateTotal();
|
||||||
}, [selectedPeriods, item]);
|
}, [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 () => {
|
const fetchItem = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -176,6 +225,59 @@ const RentItem: React.FC = () => {
|
|||||||
setFormData(prev => ({ ...prev, [name]: value }));
|
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) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
@@ -223,42 +325,117 @@ const RentItem: React.FC = () => {
|
|||||||
<h5 className="card-title">Select Rental Period</h5>
|
<h5 className="card-title">Select Rental Period</h5>
|
||||||
|
|
||||||
<AvailabilityCalendar
|
<AvailabilityCalendar
|
||||||
unavailablePeriods={[
|
unavailablePeriods={item.unavailablePeriods || []}
|
||||||
...(item.unavailablePeriods || []),
|
onPeriodsChange={() => {}} // Read-only for renters
|
||||||
...selectedPeriods.map(p => ({ ...p, isRentalSelection: true }))
|
mode="renter"
|
||||||
]}
|
selectedRentalPeriod={selectedPeriods.length > 0 ? {
|
||||||
onPeriodsChange={(periods) => {
|
id: selectedPeriods[0].id,
|
||||||
// Only handle rental selections
|
startDate: selectedPeriods[0].startDate,
|
||||||
const rentalSelections = periods.filter(p => p.isRentalSelection);
|
endDate: selectedPeriods[0].endDate,
|
||||||
setSelectedPeriods(rentalSelections.map(p => {
|
startTime: selectedPeriods[0].startTime,
|
||||||
const { isRentalSelection, ...rest } = p;
|
endTime: selectedPeriods[0].endTime,
|
||||||
return rest;
|
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 && (
|
{rentalDuration.days < minDays && rentalDuration.days > 0 && !showHourlyOptions && (
|
||||||
<div className="alert alert-warning mt-3">
|
<div className="alert alert-warning mt-3">
|
||||||
Minimum rental period is {minDays} days
|
Minimum rental period is {minDays} days
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{selectedPeriods.length === 0 && (
|
|
||||||
<div className="alert alert-info mt-3">
|
|
||||||
Please select your rental dates on the calendar above
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="card mb-4">
|
<div className="card mb-4">
|
||||||
<div className="card-body">
|
<div className="card-body">
|
||||||
<h5 className="card-title">Delivery Options</h5>
|
<div className="my-3">
|
||||||
|
|
||||||
<div className="mb-3">
|
|
||||||
<label htmlFor="deliveryMethod" className="form-label">Delivery Method *</label>
|
|
||||||
<select
|
<select
|
||||||
className="form-select"
|
className="form-select"
|
||||||
id="deliveryMethod"
|
id="deliveryMethod"
|
||||||
|
|||||||
@@ -21,8 +21,16 @@ api.interceptors.response.use(
|
|||||||
(response) => response,
|
(response) => response,
|
||||||
(error) => {
|
(error) => {
|
||||||
if (error.response?.status === 401) {
|
if (error.response?.status === 401) {
|
||||||
localStorage.removeItem('token');
|
// Only redirect to login if we have a token (user was logged in)
|
||||||
window.location.href = '/login';
|
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);
|
return Promise.reject(error);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user