added the calendre and the price toggle
All checks were successful
Build frontend / build (push) Successful in 1m7s

This commit is contained in:
mouazkh
2026-05-26 16:38:16 +03:00
parent 09bbf07d8c
commit 50a4816327

View File

@ -179,6 +179,12 @@ export default function PropertyDetailsPage() {
const [bookingError, setBookingError] = useState(null);
const [bookingSuccess, setBookingSuccess] = useState(false);
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 [avgRating, setAvgRating] = useState(null);
const [showRatingForm, setShowRatingForm] = useState(false);
@ -202,6 +208,17 @@ export default function PropertyDetailsPage() {
const mapped = mapApiDetail(data);
setProperty(mapped);
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) {
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 = () => {
setShowRatingForm(false);
if (property) fetchAvgRating(property.id);
@ -304,6 +392,8 @@ export default function PropertyDetailsPage() {
const isFav = isFavorite(property.id);
const isRoomType = property.type === 'room';
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 (
<div className="min-h-screen bg-gray-50" dir="rtl">
@ -702,27 +792,168 @@ export default function PropertyDetailsPage() {
</div>
) : (
<>
<div className="space-y-2 mb-3">
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">من تاريخ</label>
<input type="date" value={bookingDates.start} onChange={e => setBookingDates(prev => ({ ...prev, start: e.target.value }))}
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-xl focus:ring-2 focus:ring-amber-500" />
{/* Pricing Mode Toggle */}
{showPricingToggle && (
<div className="grid grid-cols-2 gap-2 mb-3">
<button onClick={() => setPricingMode('daily')}
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>
<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 }))}
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-xl focus:ring-2 focus:ring-amber-500" />
)}
{/* Step Indicator */}
<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>
{/* 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 && (
<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">
{bookingLoading ? <Loader2 className="w-4 h-4 animate-spin" /> : <Calendar className="w-4 h-4" />}
{bookingLoading ? 'جاري الحجز...' : 'حجز الآن'}
{bookingLoading ? 'جاري الحجز...' : 'تأكيد الحجز'}
</button>
</>
)}