added the calendre and the price toggle
All checks were successful
Build frontend / build (push) Successful in 1m7s
All checks were successful
Build frontend / build (push) Successful in 1m7s
This commit is contained in:
@ -179,6 +179,12 @@ export default function PropertyDetailsPage() {
|
|||||||
const [bookingError, setBookingError] = useState(null);
|
const [bookingError, setBookingError] = useState(null);
|
||||||
const [bookingSuccess, setBookingSuccess] = useState(false);
|
const [bookingSuccess, setBookingSuccess] = useState(false);
|
||||||
const [availableRanges, setAvailableRanges] = useState([]);
|
const [availableRanges, setAvailableRanges] = useState([]);
|
||||||
|
const [bookingStep, setBookingStep] = useState('entry');
|
||||||
|
const [selectedStart, setSelectedStart] = useState(null);
|
||||||
|
const [selectedEnd, setSelectedEnd] = useState(null);
|
||||||
|
const [calendarMonth, setCalendarMonth] = useState(() => new Date().getMonth());
|
||||||
|
const [calendarYear, setCalendarYear] = useState(() => new Date().getFullYear());
|
||||||
|
const [pricingMode, setPricingMode] = useState('daily');
|
||||||
const [favLoading, setFavLoading] = useState(false);
|
const [favLoading, setFavLoading] = useState(false);
|
||||||
const [avgRating, setAvgRating] = useState(null);
|
const [avgRating, setAvgRating] = useState(null);
|
||||||
const [showRatingForm, setShowRatingForm] = useState(false);
|
const [showRatingForm, setShowRatingForm] = useState(false);
|
||||||
@ -202,6 +208,17 @@ export default function PropertyDetailsPage() {
|
|||||||
const mapped = mapApiDetail(data);
|
const mapped = mapApiDetail(data);
|
||||||
setProperty(mapped);
|
setProperty(mapped);
|
||||||
if (mapped) fetchAvgRating(mapped.id);
|
if (mapped) fetchAvgRating(mapped.id);
|
||||||
|
if (mapped && mapped.isRent) {
|
||||||
|
try {
|
||||||
|
const propInfoId = mapped._raw?.propertyInformationId || mapped.id;
|
||||||
|
const ranges = await getAvailableDateRanges(propInfoId);
|
||||||
|
if (ranges && Array.isArray(ranges)) {
|
||||||
|
setAvailableRanges(ranges);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Failed to fetch date ranges', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[PropertyDetail] Failed:', err);
|
console.error('[PropertyDetail] Failed:', err);
|
||||||
@ -267,6 +284,77 @@ export default function PropertyDetailsPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const MONTHS_AR = ['يناير', 'فبراير', 'مارس', 'أبريل', 'مايو', 'يونيو', 'يوليو', 'أغسطس', 'سبتمبر', 'أكتوبر', 'نوفمبر', 'ديسمبر'];
|
||||||
|
const DAYS_AR = ['ح', 'ن', 'ث', 'ر', 'خ', 'ج', 'س'];
|
||||||
|
|
||||||
|
const availableDatesSet = useMemo(() => {
|
||||||
|
const dates = new Set();
|
||||||
|
if (!Array.isArray(availableRanges)) return dates;
|
||||||
|
availableRanges.forEach(r => {
|
||||||
|
const start = new Date(r.startDate || r.start);
|
||||||
|
const end = new Date(r.endDate || r.end);
|
||||||
|
for (let d = new Date(start); d <= end; d.setDate(d.getDate() + 1)) {
|
||||||
|
dates.add(d.toISOString().split('T')[0]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return dates;
|
||||||
|
}, [availableRanges]);
|
||||||
|
|
||||||
|
const isDateAvailable = (dateStr) => availableDatesSet.has(dateStr);
|
||||||
|
const isPastDate = (dateStr) => {
|
||||||
|
const today = new Date();
|
||||||
|
today.setHours(0, 0, 0, 0);
|
||||||
|
return new Date(dateStr) < today;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDayClick = (dateStr) => {
|
||||||
|
if (bookingStep === 'entry') {
|
||||||
|
setSelectedStart(dateStr);
|
||||||
|
setSelectedEnd(null);
|
||||||
|
setBookingStep('exit');
|
||||||
|
} else {
|
||||||
|
if (new Date(dateStr) <= new Date(selectedStart)) {
|
||||||
|
setSelectedStart(dateStr);
|
||||||
|
setSelectedEnd(null);
|
||||||
|
setBookingStep('exit');
|
||||||
|
} else {
|
||||||
|
setSelectedEnd(dateStr);
|
||||||
|
setBookingStep('entry');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBookingConfirm = async () => {
|
||||||
|
if (!AuthService.isAuthenticated()) { setShowLoginDialog(true); return; }
|
||||||
|
if (!selectedStart || !selectedEnd) {
|
||||||
|
setBookingError('يرجى تحديد تاريخ البداية والنهاية');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setBookingLoading(true);
|
||||||
|
setBookingError(null);
|
||||||
|
try {
|
||||||
|
const propInfoId = property._raw?.propertyInformationId || property.id;
|
||||||
|
const startDate = new Date(selectedStart + 'T00:00:00.000').toISOString();
|
||||||
|
const endDate = new Date(selectedEnd + 'T00:00:00.000').toISOString();
|
||||||
|
await bookReservation(propInfoId, startDate, endDate);
|
||||||
|
setBookingSuccess(true);
|
||||||
|
toast.success('تم إرسال طلب الحجز بنجاح');
|
||||||
|
} catch (err) {
|
||||||
|
setBookingError(err.message || 'فشل الحجز');
|
||||||
|
} finally {
|
||||||
|
setBookingLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const navigateMonth = (delta) => {
|
||||||
|
let month = calendarMonth + delta;
|
||||||
|
let year = calendarYear;
|
||||||
|
if (month < 0) { month = 11; year--; }
|
||||||
|
if (month > 11) { month = 0; year++; }
|
||||||
|
setCalendarMonth(month);
|
||||||
|
setCalendarYear(year);
|
||||||
|
};
|
||||||
|
|
||||||
const handleRatingSuccess = () => {
|
const handleRatingSuccess = () => {
|
||||||
setShowRatingForm(false);
|
setShowRatingForm(false);
|
||||||
if (property) fetchAvgRating(property.id);
|
if (property) fetchAvgRating(property.id);
|
||||||
@ -304,6 +392,8 @@ export default function PropertyDetailsPage() {
|
|||||||
const isFav = isFavorite(property.id);
|
const isFav = isFavorite(property.id);
|
||||||
const isRoomType = property.type === 'room';
|
const isRoomType = property.type === 'room';
|
||||||
const isMostRequested = avgRating !== null && avgRating >= 4.5;
|
const isMostRequested = avgRating !== null && avgRating >= 4.5;
|
||||||
|
const showPricingToggle = property.isRent && property.priceDisplay?.daily > 0 && property.priceDisplay?.monthly > 0;
|
||||||
|
const effectivePricingMode = showPricingToggle ? pricingMode : (property.isRent && property.priceDisplay?.monthly > 0 ? 'monthly' : 'daily');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50" dir="rtl">
|
<div className="min-h-screen bg-gray-50" dir="rtl">
|
||||||
@ -702,27 +792,168 @@ export default function PropertyDetailsPage() {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div className="space-y-2 mb-3">
|
{/* Pricing Mode Toggle */}
|
||||||
<div>
|
{showPricingToggle && (
|
||||||
<label className="block text-xs font-medium text-gray-700 mb-1">من تاريخ</label>
|
<div className="grid grid-cols-2 gap-2 mb-3">
|
||||||
<input type="date" value={bookingDates.start} onChange={e => setBookingDates(prev => ({ ...prev, start: e.target.value }))}
|
<button onClick={() => setPricingMode('daily')}
|
||||||
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-xl focus:ring-2 focus:ring-amber-500" />
|
className={`p-2.5 rounded-xl text-center border-2 transition-all ${effectivePricingMode === 'daily' ? 'border-amber-500 bg-amber-50' : 'border-gray-200 hover:border-gray-300'}`}>
|
||||||
|
<div className="text-xs font-bold text-gray-900">إيجار يومي</div>
|
||||||
|
<div className="text-sm font-bold text-amber-600">{formatCurrency(property.priceDisplay.daily)} ل.س</div>
|
||||||
|
</button>
|
||||||
|
<button onClick={() => setPricingMode('monthly')}
|
||||||
|
className={`p-2.5 rounded-xl text-center border-2 transition-all ${effectivePricingMode === 'monthly' ? 'border-amber-500 bg-amber-50' : 'border-gray-200 hover:border-gray-300'}`}>
|
||||||
|
<div className="text-xs font-bold text-gray-900">إيجار شهري</div>
|
||||||
|
<div className="text-sm font-bold text-amber-600">{formatCurrency(property.priceDisplay.monthly)} ل.س</div>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
)}
|
||||||
<label className="block text-xs font-medium text-gray-700 mb-1">إلى تاريخ</label>
|
|
||||||
<input type="date" value={bookingDates.end} onChange={e => setBookingDates(prev => ({ ...prev, end: e.target.value }))}
|
{/* Step Indicator */}
|
||||||
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-xl focus:ring-2 focus:ring-amber-500" />
|
<div className="flex items-center justify-center gap-2 mb-3">
|
||||||
|
<div className={`flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium ${bookingStep === 'entry' ? 'bg-amber-100 text-amber-800' : 'bg-gray-100 text-gray-500'}`}>
|
||||||
|
<div className={`w-1.5 h-1.5 rounded-full ${bookingStep === 'entry' ? 'bg-amber-500' : 'bg-gray-400'}`} />
|
||||||
|
تحديد تاريخ البداية
|
||||||
|
</div>
|
||||||
|
<div className="text-gray-300 text-xs">←</div>
|
||||||
|
<div className={`flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium ${bookingStep === 'exit' ? 'bg-amber-100 text-amber-800' : 'bg-gray-100 text-gray-500'}`}>
|
||||||
|
<div className={`w-1.5 h-1.5 rounded-full ${bookingStep === 'exit' ? 'bg-amber-500' : 'bg-gray-400'}`} />
|
||||||
|
تحديد تاريخ النهاية
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Calendar */}
|
||||||
|
{effectivePricingMode === 'daily' ? (
|
||||||
|
<div className="mb-3">
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<button onClick={() => navigateMonth(-1)} className="p-1 rounded-lg hover:bg-gray-100 transition-colors">
|
||||||
|
<ChevronRight className="w-4 h-4 text-gray-600" />
|
||||||
|
</button>
|
||||||
|
<span className="text-sm font-bold text-gray-900">{MONTHS_AR[calendarMonth]} {calendarYear}</span>
|
||||||
|
<button onClick={() => navigateMonth(1)} className="p-1 rounded-lg hover:bg-gray-100 transition-colors">
|
||||||
|
<ChevronLeft className="w-4 h-4 text-gray-600" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-7 mb-1">
|
||||||
|
{DAYS_AR.map((d, i) => (
|
||||||
|
<div key={i} className="text-center text-[10px] text-gray-400 font-medium py-1">{d}</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{(() => {
|
||||||
|
const firstDay = new Date(calendarYear, calendarMonth, 1).getDay();
|
||||||
|
const daysInMonth = new Date(calendarYear, calendarMonth + 1, 0).getDate();
|
||||||
|
const adjustedFirstDay = (firstDay + 1) % 7;
|
||||||
|
const cells = [];
|
||||||
|
for (let i = 0; i < adjustedFirstDay; i++) {
|
||||||
|
cells.push(<div key={`e-${i}`} />);
|
||||||
|
}
|
||||||
|
for (let day = 1; day <= daysInMonth; day++) {
|
||||||
|
const dateStr = `${calendarYear}-${String(calendarMonth + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
|
||||||
|
const past = isPastDate(dateStr);
|
||||||
|
const available = isDateAvailable(dateStr);
|
||||||
|
const isSelStart = dateStr === selectedStart;
|
||||||
|
const isSelEnd = dateStr === selectedEnd;
|
||||||
|
const inRange = selectedStart && selectedEnd && new Date(dateStr) > new Date(selectedStart) && new Date(dateStr) < new Date(selectedEnd);
|
||||||
|
const disabled = past || !available;
|
||||||
|
cells.push(
|
||||||
|
<button key={dateStr} onClick={() => !disabled && handleDayClick(dateStr)} disabled={disabled}
|
||||||
|
className={`text-center py-1.5 text-xs rounded-lg transition-all ${disabled ? 'text-gray-300 cursor-not-allowed' : ''} ${isSelStart || isSelEnd ? 'bg-amber-500 text-white font-bold shadow-sm' : ''} ${inRange ? 'bg-amber-100 text-amber-800' : ''} ${!disabled && !isSelStart && !isSelEnd && !inRange && available ? 'hover:bg-amber-50 text-gray-700' : ''} ${!disabled && !isSelStart && !isSelEnd && !inRange && !available ? 'text-red-300' : ''}`}>
|
||||||
|
{day}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return <div className="grid grid-cols-7 gap-0.5">{cells}</div>;
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="mb-3">
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<button onClick={() => setCalendarYear(prev => prev - 1)} className="p-1 rounded-lg hover:bg-gray-100 transition-colors">
|
||||||
|
<ChevronRight className="w-4 h-4 text-gray-600" />
|
||||||
|
</button>
|
||||||
|
<span className="text-sm font-bold text-gray-900">{calendarYear}</span>
|
||||||
|
<button onClick={() => setCalendarYear(prev => prev + 1)} className="p-1 rounded-lg hover:bg-gray-100 transition-colors">
|
||||||
|
<ChevronLeft className="w-4 h-4 text-gray-600" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-3 gap-2">
|
||||||
|
{MONTHS_AR.map((name, idx) => {
|
||||||
|
const monthStr = `${calendarYear}-${String(idx + 1).padStart(2, '0')}`;
|
||||||
|
const isSelStart = selectedStart && selectedStart.startsWith(monthStr);
|
||||||
|
const isSelEnd = selectedEnd && selectedEnd.startsWith(monthStr);
|
||||||
|
const inRange = selectedStart && selectedEnd && monthStr > selectedStart.substring(0, 7) && monthStr < selectedEnd.substring(0, 7);
|
||||||
|
return (
|
||||||
|
<button key={idx} onClick={() => {
|
||||||
|
const firstDay = `${monthStr}-01`;
|
||||||
|
if (bookingStep === 'entry' || (selectedStart && monthStr <= selectedStart.substring(0, 7))) {
|
||||||
|
setSelectedStart(firstDay);
|
||||||
|
setSelectedEnd(null);
|
||||||
|
setBookingStep('exit');
|
||||||
|
} else {
|
||||||
|
const lastDay = new Date(calendarYear, idx + 1, 0).getDate();
|
||||||
|
setSelectedEnd(`${monthStr}-${String(lastDay).padStart(2, '0')}`);
|
||||||
|
setBookingStep('entry');
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className={`p-2.5 rounded-xl text-center text-xs font-medium transition-all border ${isSelStart || isSelEnd ? 'bg-amber-500 text-white border-amber-500 shadow-sm' : inRange ? 'bg-amber-100 text-amber-800 border-amber-200' : 'bg-white text-gray-700 border-gray-200 hover:border-amber-300'}`}>
|
||||||
|
{name}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Summary */}
|
||||||
|
{selectedStart && (
|
||||||
|
<div className="bg-gray-50 rounded-xl p-3 mb-3 space-y-1.5">
|
||||||
|
<div className="flex justify-between text-xs">
|
||||||
|
<span className="text-gray-500">تاريخ البداية</span>
|
||||||
|
<span className="font-medium text-gray-900">{selectedStart}</span>
|
||||||
|
</div>
|
||||||
|
{selectedEnd && (
|
||||||
|
<>
|
||||||
|
<div className="flex justify-between text-xs">
|
||||||
|
<span className="text-gray-500">تاريخ النهاية</span>
|
||||||
|
<span className="font-medium text-gray-900">{selectedEnd}</span>
|
||||||
|
</div>
|
||||||
|
<div className="border-t border-gray-200 my-1" />
|
||||||
|
<div className="flex justify-between text-xs">
|
||||||
|
<span className="text-gray-500">{effectivePricingMode === 'daily' ? 'عدد الأيام' : 'عدد الأشهر'}</span>
|
||||||
|
<span className="font-medium text-gray-900">
|
||||||
|
{effectivePricingMode === 'daily'
|
||||||
|
? Math.max(1, Math.round((new Date(selectedEnd) - new Date(selectedStart)) / (1000 * 60 * 60 * 24)) + 1)
|
||||||
|
: (new Date(selectedEnd).getMonth() - new Date(selectedStart).getMonth() + (new Date(selectedEnd).getFullYear() - new Date(selectedStart).getFullYear()) * 12) + 1}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between text-xs font-bold">
|
||||||
|
<span className="text-gray-700">المجموع</span>
|
||||||
|
<span className="text-amber-600">
|
||||||
|
{formatCurrency(effectivePricingMode === 'daily'
|
||||||
|
? Math.max(1, Math.round((new Date(selectedEnd) - new Date(selectedStart)) / (1000 * 60 * 60 * 24)) + 1) * property.priceDisplay.daily
|
||||||
|
: ((new Date(selectedEnd).getMonth() - new Date(selectedStart).getMonth() + (new Date(selectedEnd).getFullYear() - new Date(selectedStart).getFullYear()) * 12) + 1) * property.priceDisplay.monthly)} ل.س
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{property.deposit > 0 && (
|
||||||
|
<div className="flex justify-between text-xs">
|
||||||
|
<span className="text-gray-500">تأمين</span>
|
||||||
|
<span className="font-medium text-gray-900">{formatCurrency(property.deposit)} ل.س</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{bookingError && (
|
{bookingError && (
|
||||||
<div className="bg-red-50 text-red-600 p-2.5 rounded-xl text-xs mb-3">{bookingError}</div>
|
<div className="bg-red-50 text-red-600 p-2.5 rounded-xl text-xs mb-3">{bookingError}</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<button onClick={handleBookNow} disabled={bookingLoading}
|
<button onClick={handleBookingConfirm} disabled={bookingLoading || !selectedStart || !selectedEnd}
|
||||||
className="w-full bg-amber-500 hover:bg-amber-600 text-white py-2.5 rounded-xl font-bold text-sm transition-all disabled:opacity-50 flex items-center justify-center gap-2">
|
className="w-full bg-amber-500 hover:bg-amber-600 text-white py-2.5 rounded-xl font-bold text-sm transition-all disabled:opacity-50 flex items-center justify-center gap-2">
|
||||||
{bookingLoading ? <Loader2 className="w-4 h-4 animate-spin" /> : <Calendar className="w-4 h-4" />}
|
{bookingLoading ? <Loader2 className="w-4 h-4 animate-spin" /> : <Calendar className="w-4 h-4" />}
|
||||||
{bookingLoading ? 'جاري الحجز...' : 'حجز الآن'}
|
{bookingLoading ? 'جاري الحجز...' : 'تأكيد الحجز'}
|
||||||
</button>
|
</button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user