Files
SweetHomeWeb/app/reservations/page.js
2026-06-24 06:36:47 -07:00

1344 lines
64 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 'use client';
// import { useState, useEffect, useCallback } from 'react';
// import { motion } from 'framer-motion';
// import { useRouter } from 'next/navigation';
// import {
// Calendar, Clock, CheckCircle, XCircle, Eye, Loader2, Search,
// MapPin, DollarSign, Home, ArrowLeft, CreditCard, Timer, Star,
// } from 'lucide-react';
// import toast, { Toaster } from 'react-hot-toast';
// import AuthService from '../services/AuthService';
// import { getRentProperties, getUserReservations, payDeposit } from '../utils/api';
// import { addPropertyRating } from '../utils/ratings';
// const API_BASE = process.env.NEXT_PUBLIC_API_URL || 'https://45.93.137.91.nip.io/api';
// const STATUS_MAP = ['pending','ownerConfirmed','depositPaid','depositConfirmed','completed','cancelled'];
// const STATUS_UI = {
// pending: { label: 'قيد الانتظار', color: 'bg-yellow-100 text-yellow-800', icon: Clock },
// ownerConfirmed: { label: 'مؤكد من المالك', color: 'bg-blue-100 text-blue-800', icon: CheckCircle },
// depositPaid: { label: 'تم دفع السلفة', color: 'bg-indigo-100 text-indigo-800', icon: DollarSign },
// depositConfirmed: { label: 'مؤكد', color: 'bg-green-100 text-green-800', icon: CheckCircle },
// completed: { label: 'منتهي', color: 'bg-green-100 text-green-800', icon: CheckCircle },
// cancelled: { label: 'ملغي', color: 'bg-gray-100 text-gray-800', icon: XCircle },
// };
// function statusLabel(code) { return STATUS_UI[STATUS_MAP[code]]?.label ?? String(code); }
// function statusColor(code) { return STATUS_UI[STATUS_MAP[code]]?.color ?? 'bg-gray-100 text-gray-700'; }
// function statusIcon(code) { return STATUS_UI[STATUS_MAP[code]]?.icon ?? Clock; }
// function StatusBadge({ code }) {
// const Icon = statusIcon(code);
// return (
// <span className={`inline-flex items-center gap-1 px-2 py-1 rounded-lg text-xs font-medium ${statusColor(code)}`}>
// <Icon className="w-3 h-3" /> {statusLabel(code)}
// </span>
// );
// }
// const propAddr = (p, r) => p?.address ?? r?.propertyAddress ?? '';
// const propImages = (p, r) => {
// if (p?.images && Array.isArray(p.images)) return p.images;
// if (r?.property?.images && Array.isArray(r.property.images)) return r.property.images;
// return [];
// };
// const propBeds = (p, r) => p?.numberOfBedRooms ?? r?.property?.numberOfBedRooms ?? 0;
// const propBaths = (p, r) => p?.numberOfBathRooms ?? r?.property?.numberOfBathRooms ?? 0;
// function parseTimeSpan(str) {
// if (!str) return 0;
// const clean = str.replace(/-/g, '');
// const dotIdx = clean.indexOf('.');
// let days = 0, timePart = clean;
// if (dotIdx !== -1) {
// days = parseInt(clean.substring(0, dotIdx), 10) || 0;
// timePart = clean.substring(dotIdx + 1);
// }
// const parts = timePart.split(':');
// if (parts.length < 2) return days * 86400000;
// const hh = parseInt(parts[0], 10) || 0;
// const mm = parseInt(parts[1], 10) || 0;
// const ss = parts.length > 2 ? (parseInt(parts[2], 10) || 0) : 0;
// return ((days * 86400) + (hh * 3600) + (mm * 60) + ss) * 1000;
// }
// function formatWindowDuration(str) {
// if (!str) return '';
// const clean = str.replace(/-/g, '');
// const dotIdx = clean.indexOf('.');
// let totalHours = 0, timePart = clean;
// if (dotIdx !== -1) {
// const days = parseInt(clean.substring(0, dotIdx), 10) || 0;
// totalHours += days * 24;
// timePart = clean.substring(dotIdx + 1);
// }
// const parts = timePart.split(':');
// if (parts.length >= 2) {
// totalHours += parseInt(parts[0], 10) || 0;
// }
// if (totalHours > 0) return `${String(totalHours).padStart(2, '0')}:00:00`;
// return timePart.substring(0, 8);
// }
// function CountdownTimer({ deadline }) {
// const [remaining, setRemaining] = useState(deadline ? Math.max(0, deadline - Date.now()) : 0);
// useEffect(() => {
// if (!deadline) return;
// const tick = () => setRemaining(Math.max(0, deadline - Date.now()));
// tick();
// const id = setInterval(tick, 1000);
// return () => clearInterval(id);
// }, [deadline]);
// if (remaining <= 0) return <span className="text-red-500 text-sm font-medium">انتهت المهلة</span>;
// const h = Math.floor(remaining / 3600000);
// const m = Math.floor((remaining % 3600000) / 60000);
// const s = Math.floor((remaining % 60000) / 1000);
// const pad = (n) => String(n).padStart(2, '0');
// return <span className="text-amber-600 text-sm font-mono font-bold" dir="ltr">{pad(h)}:{pad(m)}:{pad(s)}</span>;
// }
// function ReservationCard({ r, onViewDetails, onPay, payingId }) {
// const p = r._prop;
// const imgs = propImages(p, r);
// const img = imgs.length > 0 ? `${API_BASE}${imgs[0]}` : null;
// const addr = propAddr(p, r);
// const beds = propBeds(p, r);
// const baths = propBaths(p, r);
// const isOwnerConfirmed = STATUS_MAP[r.status] === 'ownerConfirmed';
// const canRate = STATUS_MAP[r.status] === 'depositPaid' || STATUS_MAP[r.status] === 'completed';
// const hasTimeWindow = r.ownerApprovalDate && p?.allowedPaymentPeriod;
// const deadline = hasTimeWindow
// ? new Date(r.ownerApprovalDate).getTime() + parseTimeSpan(p.allowedPaymentPeriod)
// : null;
// const isExpired = deadline ? Date.now() > deadline : false;
// const isPaying = payingId === r.id;
// const [showRating, setShowRating] = useState(false);
// const [ratings, setRatings] = useState({ clean: 0, services: 0, ownerBehavior: 0, experience: 0 });
// const [ratingComment, setRatingComment] = useState('');
// const [submittingRating, setSubmittingRating] = useState(false);
// return (
// <motion.div initial={{ opacity:0,y:20 }} animate={{ opacity:1,y:0 }}
// className="bg-white rounded-2xl shadow-sm hover:shadow-md transition-all border border-gray-200 overflow-hidden">
// <div className="p-5">
// {img && <div className="mb-4 w-full h-40 rounded-xl overflow-hidden"><img src={img} alt="" className="w-full h-full object-cover" /></div>}
// <div className="flex justify-between items-start mb-3">
// <div>
// <StatusBadge code={r.status} />
// {addr && <div className="flex items-center gap-1 text-gray-500 text-sm mt-1"><MapPin className="w-4 h-4"/>{addr}</div>}
// </div>
// <div className="text-left">
// <div className="text-lg font-bold text-amber-600">{r.totalPrice?.toLocaleString() ?? '—'}</div>
// <div className="text-xs text-gray-500">السعر الإجمالي</div>
// </div>
// </div>
// {(beds||baths) && <div className="flex gap-3 mb-3 text-sm text-gray-600">{beds>0&&<span>{beds} غرف</span>}{baths>0&&<span>{baths} حمامات</span>}</div>}
// <div className="grid grid-cols-2 gap-3 mb-4 text-center">
// <div className="bg-gray-50 p-2 rounded-lg">
// <Calendar className="w-4 h-4 text-amber-500 mx-auto mb-1"/><div className="text-xs text-gray-500">من</div>
// <div className="text-sm font-medium">{new Date(r.startDate).toLocaleDateString('ar')}</div>
// </div>
// <div className="bg-gray-50 p-2 rounded-lg">
// <Calendar className="w-4 h-4 text-amber-500 mx-auto mb-1"/><div className="text-xs text-gray-500">إلى</div>
// <div className="text-sm font-medium">{new Date(r.endDate).toLocaleDateString('ar')}</div>
// </div>
// </div>
// {isOwnerConfirmed && hasTimeWindow && <div className="bg-blue-50 p-3 rounded-xl mb-3">
// <div className="flex items-center justify-between mb-1">
// <span className="text-sm text-blue-800 font-medium flex items-center gap-1"><Timer className="w-4 h-4"/> متبقي للدفع:</span>
// <CountdownTimer deadline={deadline} />
// </div>
// <div className="text-xs text-blue-600">مدة الدفع: {formatWindowDuration(p.allowedPaymentPeriod)}</div>
// </div>}
// <div className="flex gap-3 pt-3 border-t border-gray-100">
// <button onClick={() => onViewDetails(r)}
// className="flex-1 bg-gray-100 text-gray-700 py-2 rounded-xl text-sm font-medium hover:bg-gray-200 transition-colors flex items-center justify-center gap-2">
// <Eye className="w-4 h-4"/> التفاصيل
// </button>
// {isOwnerConfirmed && !isExpired && <button onClick={() => onPay(r)} disabled={isPaying}
// className={`flex-1 py-2 rounded-xl text-sm font-medium transition-colors flex items-center justify-center gap-2 ${isPaying ? 'bg-gray-300 text-gray-500 cursor-not-allowed' : 'bg-amber-500 text-white hover:bg-amber-600'}`}>
// {isPaying ? <Loader2 className="w-4 h-4 animate-spin"/> : <CreditCard className="w-4 h-4"/>} {isPaying ? 'جاري الدفع...' : 'ادفع الآن'}
// </button>}
// </div>
// {canRate && !showRating && <button onClick={() => setShowRating(true)}
// className="w-full mt-3 bg-amber-50 text-amber-700 py-2 rounded-xl text-sm font-medium hover:bg-amber-100 transition-colors flex items-center justify-center gap-2">
// <Star className="w-4 h-4"/> قيّم هذا العقار
// </button>}
// {canRate && showRating && <div className="mt-3 bg-amber-50 p-3 rounded-xl">
// <div className="space-y-2 mb-3">
// {[
// { key: 'clean', label: 'النظافة' },
// { key: 'services', label: 'الخدمات' },
// { key: 'ownerBehavior', label: 'تعامل المالك' },
// { key: 'experience', label: 'التجربة العامة' },
// ].map(cat => <div key={cat.key} className="flex items-center justify-between">
// <span className="text-sm text-gray-700">{cat.label}</span>
// <div className="flex gap-0.5">
// {[1,2,3,4,5].map(n => (
// <button key={n} onClick={() => setRatings(p => ({...p, [cat.key]: n}))}
// className={`p-0.5 rounded-full transition-colors ${n <= ratings[cat.key] ? 'text-amber-500' : 'text-gray-300'}`}>
// <Star className={`w-4 h-4 ${n <= ratings[cat.key] ? 'fill-amber-500' : ''}`} />
// </button>
// ))}
// </div>
// </div>)}
// </div>
// <textarea value={ratingComment} onChange={e => setRatingComment(e.target.value)}
// placeholder="أكتب تعليقك (اختياري)"
// className="w-full p-2 text-sm border border-amber-200 rounded-lg resize-none focus:outline-none focus:ring-2 focus:ring-amber-500 mb-2" rows={2} />
// <div className="flex gap-2">
// <button onClick={async () => {
// if (!ratings.clean || !ratings.services || !ratings.ownerBehavior || !ratings.experience) return toast.error('قيّم جميع الفئات');
// setSubmittingRating(true);
// try {
// await addPropertyRating({ reservationId: r.id, cleanRating: ratings.clean, servicesRating: ratings.services, ownerBehaviorRating: ratings.ownerBehavior, experienceRating: ratings.experience, comment: ratingComment || null });
// toast.success('تم إرسال التقييم');
// setShowRating(false);
// setRatings({ clean: 0, services: 0, ownerBehavior: 0, experience: 0 });
// setRatingComment('');
// } catch (e) { toast.error(e?.message || 'فشل إرسال التقييم'); }
// finally { setSubmittingRating(false); }
// }} disabled={submittingRating}
// className="flex-1 bg-amber-500 text-white py-1.5 rounded-lg text-sm font-medium hover:bg-amber-600 transition-colors disabled:bg-gray-300">
// {submittingRating ? 'جاري الإرسال...' : 'إرسال التقييم'}
// </button>
// <button onClick={() => { setShowRating(false); setRatings({ clean: 0, services: 0, ownerBehavior: 0, experience: 0 }); setRatingComment(''); }}
// className="px-4 py-1.5 bg-gray-200 text-gray-700 rounded-lg text-sm hover:bg-gray-300 transition-colors">إلغاء</button>
// </div>
// </div>}
// </div>
// </motion.div>
// );
// }
// function DetailsModal({ r, isOpen, onClose, onPay, payingId }) {
// if (!isOpen || !r) return null;
// const p = r._prop;
// const isOwnerConfirmed = STATUS_MAP[r.status] === 'ownerConfirmed';
// const hasTimeWindow = r.ownerApprovalDate && p?.allowedPaymentPeriod;
// const deadline = hasTimeWindow
// ? new Date(r.ownerApprovalDate).getTime() + parseTimeSpan(p.allowedPaymentPeriod)
// : null;
// const isExpired = deadline ? Date.now() > deadline : false;
// const isPaying = payingId === r.id;
// return (
// <motion.div initial={{opacity:0}} animate={{opacity:1}} exit={{opacity:0}}
// className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center p-4 z-50" onClick={onClose}>
// <motion.div initial={{scale:0.9,y:20}} animate={{scale:1,y:0}} exit={{scale:0.9,y:20}}
// className="bg-white rounded-2xl w-full max-w-2xl max-h-[90vh] overflow-y-auto shadow-2xl" onClick={e=>e.stopPropagation()}>
// <div className="sticky top-0 bg-gradient-to-r from-amber-500 to-amber-600 p-6 text-white">
// <div className="flex justify-between items-center">
// <h2 className="text-xl font-bold">تفاصيل الحجز</h2>
// <button onClick={onClose} className="p-1 hover:bg-white/20 rounded-full"><XCircle className="w-6 h-6"/></button>
// </div>
// <p className="text-amber-100 text-sm mt-1">رقم الحجز: #{r.id}</p>
// </div>
// <div className="p-6 space-y-6">
// {p && <div className="bg-gray-50 p-4 rounded-xl">
// <h3 className="font-bold text-gray-900 mb-3 flex items-center gap-2"><Home className="w-5 h-5 text-amber-500"/> معلومات العقار</h3>
// <p><span className="text-gray-500">العنوان:</span> {propAddr(p, r)||'—'}</p>
// {(propBeds(p, r)||propBaths(p, r)) && <div className="flex gap-3 mt-2">
// {propBeds(p, r)>0&&<span className="text-sm bg-white px-2 py-1 rounded-lg">{propBeds(p, r)} غرف</span>}
// {propBaths(p, r)>0&&<span className="text-sm bg-white px-2 py-1 rounded-lg">{propBaths(p, r)} حمامات</span>}
// </div>}
// </div>}
// <div className="bg-gray-50 p-4 rounded-xl">
// <h3 className="font-bold text-gray-900 mb-3 flex items-center gap-2"><Calendar className="w-5 h-5 text-amber-500"/> تفاصيل الحجز</h3>
// <div className="grid grid-cols-2 gap-4">
// <div><p className="text-gray-500">تاريخ البداية</p><p className="font-medium">{new Date(r.startDate).toLocaleDateString('ar')}</p></div>
// <div><p className="text-gray-500">تاريخ النهاية</p><p className="font-medium">{new Date(r.endDate).toLocaleDateString('ar')}</p></div>
// <div><p className="text-gray-500">الحالة</p><StatusBadge code={r.status}/></div>
// <div><p className="text-gray-500">تاريخ الإنشاء</p><p className="font-medium">{new Date(r.createdAt).toLocaleDateString('ar')}</p></div>
// </div>
// </div>
// <div className="bg-amber-50 p-4 rounded-xl">
// <h3 className="font-bold text-amber-700 mb-3 flex items-center gap-2"><DollarSign className="w-5 h-5"/> المعلومات المالية</h3>
// <div className="flex justify-between font-bold"><span className="text-gray-900">الإجمالي</span><span className="text-amber-600 text-lg">{r.totalPrice?.toLocaleString()??'—'}</span></div>
// </div>
// {isOwnerConfirmed && hasTimeWindow && <div className="bg-blue-50 p-4 rounded-xl">
// <div className="flex items-center justify-between mb-2">
// <span className="text-blue-800 font-medium flex items-center gap-2"><Timer className="w-5 h-5"/> متبقي للدفع:</span>
// <CountdownTimer deadline={deadline} />
// </div>
// <div className="text-xs text-blue-600 mb-3">مدة الدفع: {formatWindowDuration(p.allowedPaymentPeriod)}</div>
// {!isExpired && <button onClick={() => { onPay(r); onClose(); }} disabled={isPaying}
// className={`w-full py-2 rounded-xl font-medium transition-colors flex items-center justify-center gap-2 ${isPaying ? 'bg-gray-300 text-gray-500 cursor-not-allowed' : 'bg-amber-500 text-white hover:bg-amber-600'}`}>
// {isPaying ? <Loader2 className="w-5 h-5 animate-spin"/> : <CreditCard className="w-5 h-5"/>} {isPaying ? 'جاري الدفع...' : 'ادفع الآن'}
// </button>}
// </div>}
// </div>
// </motion.div>
// </motion.div>
// );
// }
// export default function UserReservationsPage() {
// const router = useRouter();
// const [reservations, setReservations] = useState([]);
// const [filtered, setFiltered] = useState([]);
// const [loading, setLoading] = useState(true);
// const [selected, setSelected] = useState(null);
// const [filterStatus, setFilterStatus] = useState('all');
// const [searchTerm, setSearchTerm] = useState('');
// const [payingId, setPayingId] = useState(null);
// useEffect(() => { if (!AuthService.getUser()) { router.push('/login'); return; } loadReservations(); }, [router]);
// const loadReservations = useCallback(async () => {
// try {
// const [data, rentProps] = await Promise.all([
// getUserReservations(),
// getRentProperties().catch(() => []),
// ]);
// const list = Array.isArray(data) ? data : [];
// const propsList = Array.isArray(rentProps) ? rentProps : [];
// const propMap = {};
// propsList.forEach(rp => {
// const info = rp?.propertyInformation ?? {};
// if (rp?.allowedPaymentPeriod) info.allowedPaymentPeriod = rp.allowedPaymentPeriod;
// propMap[rp.propertyInformationId] = info;
// propMap[rp.propertyInformation?.id] = info;
// });
// const enriched = list.map(r => {
// if (r.propertyId && propMap[r.propertyId]) r._prop = propMap[r.propertyId];
// return r;
// });
// setReservations(enriched);
// setFiltered(enriched);
// } catch (err) {
// console.error(err);
// toast.error('فشل تحميل الحجوزات');
// setReservations([]);
// setFiltered([]);
// }
// setLoading(false);
// }, []);
// useEffect(() => {
// let r = reservations;
// if (filterStatus !== 'all') r = r.filter(x => STATUS_MAP[x.status] === filterStatus);
// if (searchTerm) { const q = searchTerm.toLowerCase(); r = r.filter(x => propAddr(x._prop, x).toLowerCase().includes(q) || String(x.id).includes(q)); }
// setFiltered(r);
// }, [reservations, filterStatus, searchTerm]);
// const allStatuses = [...new Set(reservations.map(r => STATUS_MAP[r.status]))];
// const counts = { all: reservations.length, ...Object.fromEntries(allStatuses.map(s => [s, reservations.filter(r => STATUS_MAP[r.status] === s).length])) };
// const handlePay = async (r) => {
// setPayingId(r.id);
// try {
// await payDeposit({
// reservationId: r.id,
// paymentTypeId: 1,
// transactionType: 1,
// comment: null,
// });
// toast.success('تم دفع السلفة بنجاح!');
// loadReservations();
// } catch (err) {
// toast.error(err?.message || 'فشل عملية الدفع');
// } finally {
// setPayingId(null);
// }
// };
// if (loading) return <div className="min-h-screen bg-gray-50 flex items-center justify-center"><Loader2 className="w-12 h-12 text-amber-500 animate-spin"/></div>;
// return (
// <div className="min-h-screen bg-gray-50 py-8" dir="rtl">
// <Toaster position="top-center" reverseOrder={false} />
// <DetailsModal r={selected} isOpen={!!selected} onClose={() => setSelected(null)} onPay={handlePay} payingId={payingId} />
// <div className="container mx-auto px-4">
// <motion.div initial={{opacity:0,y:-20}} animate={{opacity:1,y:0}} className="mb-8">
// <button onClick={() => router.back()} className="flex items-center gap-2 text-gray-600 hover:text-amber-600 mb-4"><ArrowLeft className="w-5 h-5"/> الرجوع</button>
// <h1 className="text-3xl font-bold text-gray-900 mb-2">حجوزاتي</h1>
// <p className="text-gray-600">لديك {reservations.length} حجز</p>
// </motion.div>
// <div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8">
// {Object.entries(counts).map(([s, c]) => (
// <motion.div key={s} initial={{opacity:0,y:20}} animate={{opacity:1,y:0}}
// className={`bg-white rounded-xl shadow-sm p-4 text-center border cursor-pointer hover:shadow-md transition-all ${filterStatus===s?'border-amber-500 bg-amber-50':'border-gray-200'}`}
// onClick={() => setFilterStatus(s)}>
// <div className="text-2xl font-bold text-amber-600">{c}</div>
// <div className="text-sm text-gray-600">{s==='all'?'الكل':(STATUS_UI[s]?.label||s)}</div>
// </motion.div>
// ))}
// </div>
// <div className="mb-6 relative">
// <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400"/>
// <input type="text" placeholder="ابحث بعنوان العقار أو رقم الحجز..." value={searchTerm} onChange={e=>setSearchTerm(e.target.value)}
// className="w-full pl-10 pr-4 py-3 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500"/>
// </div>
// {filtered.length === 0 ? (
// <div className="bg-white rounded-2xl p-12 text-center border-2 border-dashed border-gray-300">
// <Calendar className="w-12 h-12 text-amber-600 mx-auto mb-4"/>
// <h3 className="text-xl font-bold text-gray-900 mb-2">لا توجد حجوزات</h3>
// <p className="text-gray-600">لم تقم بأي حجز حتى الآن</p>
// </div>
// ) : (
// <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
// {filtered.map(r => <ReservationCard key={r.id} r={r} onViewDetails={setSelected} onPay={handlePay} payingId={payingId} />)}
// </div>
// )}
// </div>
// </div>
// );
// }
'use client';
import { useState, useEffect, useCallback } from 'react';
import Link from 'next/link';
import { motion } from 'framer-motion';
import { useRouter } from 'next/navigation';
import {
Calendar,
Clock,
CheckCircle,
XCircle,
Eye,
Loader2,
Search,
MapPin,
DollarSign,
Home,
ArrowLeft,
CreditCard,
Timer,
Star,
Flag,
Banknote,
Building,
ArrowLeftRight,
Check,
X,
Info,
Landmark,
Phone,
Wallet,
} from 'lucide-react';
import toast, { Toaster } from 'react-hot-toast';
import AuthService from '../services/AuthService';
import { getRentProperties, getUserReservations, payDeposit } from '../utils/api';
import { addPropertyRating } from '../utils/ratings';
const API_BASE = process.env.NEXT_PUBLIC_API_URL || 'https://45.93.137.91.nip.io/api';
const STATUS_MAP = ['pending','ownerConfirmed','depositPaid','depositConfirmed','completed','cancelled'];
const STATUS_UI = {
pending: { label: 'قيد الانتظار', color: 'bg-yellow-100 text-yellow-800', icon: Clock },
ownerConfirmed: { label: 'مؤكد من المالك', color: 'bg-blue-100 text-blue-800', icon: CheckCircle },
depositPaid: { label: 'تم دفع السلفة', color: 'bg-indigo-100 text-indigo-800', icon: DollarSign },
depositConfirmed: { label: 'مؤكد', color: 'bg-green-100 text-green-800', icon: CheckCircle },
completed: { label: 'منتهي', color: 'bg-green-100 text-green-800', icon: CheckCircle },
cancelled: { label: 'ملغي', color: 'bg-gray-100 text-gray-800', icon: XCircle },
};
const PAYMENT_METHODS = [
{
id: 'cash',
label: 'الدفع النقدي',
desc: 'ادفع الآن عبر شام كاش مع تأكيد تلقائي للحجز',
icon: Banknote,
active: true,
},
{
id: 'office',
label: 'نقداً',
desc: 'ادفع في مكتب المنصة. يتم التأكيد لاحقاً',
icon: Building,
active: true,
},
{
id: 'transfer',
label: 'تحويل داخلي',
desc: 'حول من حساب داخلي ثم أرسل المرجع',
icon: ArrowLeftRight,
active: false,
},
{
id: 'electronic',
label: 'دفع إلكتروني',
desc: 'سيتم تفعيل الدفع الإلكتروني والتحويل لاحقاً',
icon: CreditCard,
active: false,
},
];
function statusLabel(code) { return STATUS_UI[STATUS_MAP[code]]?.label ?? String(code); }
function statusColor(code) { return STATUS_UI[STATUS_MAP[code]]?.color ?? 'bg-gray-100 text-gray-700'; }
function statusIcon(code) { return STATUS_UI[STATUS_MAP[code]]?.icon ?? Clock; }
function formatCurrency(v, sign = 'ل.س') {
return `${sign} ${Number(v ?? 0).toLocaleString()}`;
}
function formatDate(date) {
if (!date) return '';
const d = new Date(date);
if (Number.isNaN(d.getTime())) return '';
return d.toLocaleDateString('en-GB');
}
function StatusBadge({ code }) {
const Icon = statusIcon(code);
return (
<span className={`inline-flex items-center gap-1 px-2 py-1 rounded-lg text-xs font-medium ${statusColor(code)}`}>
<Icon className="w-3 h-3" /> {statusLabel(code)}
</span>
);
}
const propAddr = (p, r) => p?.address ?? r?.propertyAddress ?? '';
const propImages = (p, r) => {
if (p?.images && Array.isArray(p.images)) return p.images;
if (r?.property?.images && Array.isArray(r.property.images)) return r.property.images;
return [];
};
const propBeds = (p, r) => p?.numberOfBedRooms ?? r?.property?.numberOfBedRooms ?? 0;
const propBaths = (p, r) => p?.numberOfBathRooms ?? r?.property?.numberOfBathRooms ?? 0;
const getAuthToken = () => {
if (typeof window === 'undefined') return '';
return (
AuthService.getToken?.() ||
AuthService.getAccessToken?.() ||
localStorage.getItem('token') ||
localStorage.getItem('accessToken') ||
localStorage.getItem('authToken') ||
''
);
};
const readStoredUser = () => {
if (typeof window === 'undefined') return null;
const keys = ['user', 'currentUser', 'authUser', 'profile'];
for (const key of keys) {
const raw = localStorage.getItem(key);
if (!raw) continue;
try {
return JSON.parse(raw);
} catch {
return raw;
}
}
return null;
};
const extractNumericUserId = (value) => {
if (!value) return null;
if (typeof value === 'number') return Number.isInteger(value) ? value : null;
if (typeof value === 'string') {
const n = Number(value);
return Number.isInteger(n) ? n : null;
}
if (typeof value === 'object') {
const candidates = [
value.id,
value.userId,
value.userID,
value.user?.id,
value.user?.userId,
value.profile?.id,
value.profile?.userId,
value.data?.id,
];
for (const candidate of candidates) {
const id = extractNumericUserId(candidate);
if (id !== null) return id;
}
}
return null;
};
async function reportReservation(reservationId, message) {
const user = AuthService.getUser?.() ?? readStoredUser();
const reporter = extractNumericUserId(user);
const rid = Number(reservationId);
if (!Number.isInteger(rid)) {
throw new Error('رقم الحجز غير صالح');
}
if (!Number.isInteger(reporter)) {
throw new Error('تعذر تحديد المستخدم الحالي');
}
const token = getAuthToken();
const res = await fetch(`${API_BASE}/ReservationReports/ReportReservation`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...(token ? { Authorization: `Bearer ${token}` } : {}),
},
body: JSON.stringify({
reservationId: rid,
message: message ?? null,
reporter,
}),
});
if (!res.ok) {
let errorMessage = 'فشل إرسال البلاغ';
try {
const data = await res.json();
errorMessage = data?.message || data?.title || errorMessage;
} catch (_) {
try {
const text = await res.text();
if (text) errorMessage = text;
} catch (_) {}
}
throw new Error(errorMessage);
}
try {
return await res.json();
} catch {
return null;
}
}
function parseTimeSpan(str) {
if (!str) return 0;
const clean = str.replace(/-/g, '');
const dotIdx = clean.indexOf('.');
let days = 0, timePart = clean;
if (dotIdx !== -1) {
days = parseInt(clean.substring(0, dotIdx), 10) || 0;
timePart = clean.substring(dotIdx + 1);
}
const parts = timePart.split(':');
if (parts.length < 2) return days * 86400000;
const hh = parseInt(parts[0], 10) || 0;
const mm = parseInt(parts[1], 10) || 0;
const ss = parts.length > 2 ? (parseInt(parts[2], 10) || 0) : 0;
return ((days * 86400) + (hh * 3600) + (mm * 60) + ss) * 1000;
}
function formatWindowDuration(str) {
if (!str) return '';
const clean = str.replace(/-/g, '');
const dotIdx = clean.indexOf('.');
let totalHours = 0, timePart = clean;
if (dotIdx !== -1) {
const days = parseInt(clean.substring(0, dotIdx), 10) || 0;
totalHours += days * 24;
timePart = clean.substring(dotIdx + 1);
}
const parts = timePart.split(':');
if (parts.length >= 2) {
totalHours += parseInt(parts[0], 10) || 0;
}
if (totalHours > 0) return `${String(totalHours).padStart(2, '0')}:00:00`;
return timePart.substring(0, 8);
}
function CountdownTimer({ deadline }) {
const [remaining, setRemaining] = useState(deadline ? Math.max(0, deadline - Date.now()) : 0);
useEffect(() => {
if (!deadline) return;
const tick = () => setRemaining(Math.max(0, deadline - Date.now()));
tick();
const id = setInterval(tick, 1000);
return () => clearInterval(id);
}, [deadline]);
if (remaining <= 0) return <span className="text-red-500 text-sm font-medium">انتهت المهلة</span>;
const h = Math.floor(remaining / 3600000);
const m = Math.floor((remaining % 3600000) / 60000);
const s = Math.floor((remaining % 60000) / 1000);
const pad = (n) => String(n).padStart(2, '0');
return <span className="text-amber-600 text-sm font-mono font-bold" dir="ltr">{pad(h)}:{pad(m)}:{pad(s)}</span>;
}
function PaymentDialog({
isOpen,
reservation,
onClose,
selectedPayment,
onSelectPayment,
onConfirmPay,
payingId,
}) {
const [showCashDialog, setShowCashDialog] = useState(false);
useEffect(() => {
if (isOpen) setShowCashDialog(false);
}, [isOpen, reservation?.id]);
if (!isOpen || !reservation) return null;
const amount = reservation.depositAmount || reservation.totalPrice || 0;
const currencySign = reservation.currencySign || 'ل.س';
return (
<>
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 px-4 py-8"
onClick={onClose}
>
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
className="w-full max-w-2xl rounded-3xl bg-white shadow-2xl border border-gray-200 overflow-hidden"
onClick={(e) => e.stopPropagation()}
>
<div className="p-6 border-b border-gray-100">
<div className="flex items-start gap-4 mb-4">
<div className="w-14 h-14 bg-amber-100 rounded-2xl flex items-center justify-center shrink-0">
<Home className="w-7 h-7 text-amber-600" />
</div>
<div className="min-w-0 flex-1">
<h3 className="text-xl font-bold text-gray-900 mb-1">
{reservation.propertyName}
</h3>
{(reservation.propertyAddress || reservation.propertyCity) && (
<p className="text-sm text-gray-500 flex items-center gap-1">
<MapPin className="w-3.5 h-3.5" />
{[reservation.propertyCity, reservation.propertyAddress].filter(Boolean).join(' - ')}
</p>
)}
</div>
</div>
<div className="flex items-center gap-4 text-sm text-gray-600">
<span className="flex items-center gap-1.5">
<Calendar className="w-4 h-4 text-gray-400" />
{formatDate(reservation.startDate)} - {formatDate(reservation.endDate)}
</span>
<span className="flex items-center gap-1.5">
<Clock className="w-4 h-4 text-gray-400" />
#{reservation.reservationId || reservation.id}
</span>
</div>
</div>
<div className="px-6 py-5 border-b border-gray-100">
<div className="text-center">
<p className="text-sm text-gray-500 mb-1">مبلغ التأمين (الرعبون)</p>
<p className="text-4xl font-bold text-amber-600">
{formatCurrency(amount, currencySign)}
</p>
<p className="text-xs text-gray-400 mt-2">
يتم دفع التأمين إلى المنصة وليس إلى المالك
</p>
</div>
</div>
<div className="px-6 py-4 bg-amber-50 border-b border-amber-100">
<div className="flex items-start gap-3">
<Info className="w-5 h-5 text-amber-600 shrink-0 mt-0.5" />
<p className="text-sm text-amber-800 leading-relaxed">
الدفع النقدي فقط هو المتاح. سيتم تفعيل الدفع الإلكتروني والتحويل لاحقاً.
</p>
</div>
</div>
<div className="px-6 py-5 border-b border-gray-100">
<p className="text-sm font-bold text-gray-700 mb-3">طريقة الدفع</p>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
{PAYMENT_METHODS.map((method) => {
const isSelected = selectedPayment === method.id;
const Icon = method.icon;
return (
<button
key={method.id}
type="button"
disabled={!method.active}
onClick={() => method.active && onSelectPayment(method.id)}
className={`relative flex items-start gap-3 p-4 rounded-2xl border-2 text-right transition-all ${
!method.active
? 'border-gray-100 bg-gray-50 opacity-50 cursor-not-allowed'
: isSelected
? 'border-amber-500 bg-amber-50 shadow-sm'
: 'border-gray-200 bg-white hover:border-amber-300 cursor-pointer'
}`}
>
<div className={`w-10 h-10 rounded-xl flex items-center justify-center shrink-0 ${
isSelected ? 'bg-amber-500 text-white' : 'bg-gray-100 text-gray-500'
}`}>
<Icon className="w-5 h-5" />
</div>
<div className="min-w-0 flex-1">
<p className={`text-sm font-bold ${isSelected ? 'text-amber-900' : 'text-gray-800'}`}>
{method.label}
</p>
<p className="text-xs text-gray-500 mt-0.5 leading-relaxed">
{method.desc}
</p>
{!method.active && (
<span className="inline-block mt-1 text-[10px] font-medium text-gray-400 bg-gray-200 px-2 py-0.5 rounded-full">
غير متاح
</span>
)}
</div>
{isSelected && (
<div className="absolute top-2 left-2 w-5 h-5 bg-amber-500 rounded-full flex items-center justify-center">
<Check className="w-3 h-3 text-white" />
</div>
)}
</button>
);
})}
</div>
</div>
<div className="px-6 py-5 border-b border-gray-100">
<motion.button
type="button"
onClick={() => onConfirmPay(reservation)}
disabled={payingId === reservation.id}
whileHover={{ scale: 1.01 }}
whileTap={{ scale: 0.99 }}
className="w-full bg-amber-500 hover:bg-amber-600 disabled:bg-amber-300 text-white font-bold py-4 px-6 rounded-2xl text-lg transition-all shadow-lg hover:shadow-xl flex items-center justify-center gap-3"
>
{payingId === reservation.id ? (
<>
<Loader2 className="w-5 h-5 animate-spin" />
جاري الدفع...
</>
) : (
<>
<Banknote className="w-5 h-5" />
دفع التأمين ({formatCurrency(amount, currencySign)})
</>
)}
</motion.button>
</div>
<div className="px-6 py-4 border-b border-gray-100">
<button
type="button"
onClick={() => setShowCashDialog(true)}
className="w-full bg-white border-2 border-amber-200 hover:border-amber-400 text-amber-700 font-bold py-3 px-6 rounded-2xl text-base transition-all flex items-center justify-center gap-2"
>
<Building className="w-5 h-5" />
سأدفع نقداً
</button>
</div>
<div className="px-6 py-5 bg-gray-50">
<div className="flex items-start gap-3">
<Landmark className="w-5 h-5 text-amber-600 shrink-0 mt-0.5" />
<div>
<p className="text-sm font-bold text-gray-800 mb-1">موقع الدفع النقدي</p>
<p className="text-sm text-gray-600 leading-relaxed">
مكتب المنصة: أبو رمانة، شارع المالكي، دمشق
</p>
<p className="text-sm text-gray-600 flex items-center gap-1 mt-1">
<Phone className="w-3.5 h-3.5" />
<span dir="ltr">+963567823411</span>
</p>
</div>
</div>
</div>
<div className="p-4 flex justify-end">
<button
type="button"
onClick={onClose}
className="px-5 py-2.5 rounded-xl border border-gray-300 text-gray-700 hover:bg-gray-100 transition-colors font-medium"
>
إغلاق
</button>
</div>
</motion.div>
</div>
{showCashDialog && (
<div className="fixed inset-0 z-[60] flex items-center justify-center bg-black/60 px-4 py-8">
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
className="w-full max-w-md rounded-3xl bg-white p-8 shadow-2xl border border-gray-200 text-center"
>
<div className="w-16 h-16 bg-amber-100 rounded-full flex items-center justify-center mx-auto mb-5">
<Clock className="w-8 h-8 text-amber-600" />
</div>
<h3 className="text-xl font-bold text-gray-900 mb-3">الدفع النقدي قيد الانتظار</h3>
<p className="text-gray-600 leading-relaxed mb-6">
اذهب إلى موقع المنصة وادفع العربون.
<br />
ستقوم المنصة بتأكيد الدفع بعد استلام المبلغ.
</p>
<div className="bg-gray-50 rounded-2xl p-4 mb-6 text-right">
<p className="text-sm font-bold text-gray-800 mb-1">موقع المنصة</p>
<p className="text-sm text-gray-600">مكتب المنصة: أبو رمانة، شارع المالكي، دمشق</p>
<p className="text-sm text-gray-600 flex items-center gap-1 mt-1" dir="ltr">
<Phone className="w-3.5 h-3.5" />
+963567823411
</p>
</div>
<div className="flex flex-col gap-3 sm:flex-row">
<button
type="button"
onClick={() => setShowCashDialog(false)}
className="w-full px-5 py-3 rounded-xl border border-gray-300 text-gray-700 hover:bg-gray-100 transition-colors font-medium"
>
إغلاق
</button>
<Link
href="/reservations"
className="w-full px-5 py-3 rounded-xl bg-amber-500 text-white font-semibold text-center hover:bg-amber-600 transition-colors"
>
حجوزاتي
</Link>
</div>
</motion.div>
</div>
)}
</>
);
}
function ReportDialog({ isOpen, reservation, onClose, onSubmit, submitting }) {
const [message, setMessage] = useState('');
useEffect(() => {
if (isOpen) setMessage('');
}, [isOpen, reservation?.id]);
if (!isOpen || !reservation) return null;
return (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm p-4"
onClick={onClose}
>
<motion.div
initial={{ scale: 0.95, y: 16 }}
animate={{ scale: 1, y: 0 }}
exit={{ scale: 0.95, y: 16 }}
className="w-full max-w-lg rounded-2xl bg-white shadow-2xl overflow-hidden"
onClick={(e) => e.stopPropagation()}
>
<div className="bg-gradient-to-r from-red-500 to-red-600 p-6 text-white">
<div className="flex items-center justify-between">
<div>
<h2 className="text-xl font-bold">الإبلاغ عن الحجز</h2>
<p className="text-red-100 text-sm mt-1">رقم الحجز: #{reservation.id}</p>
</div>
<button onClick={onClose} className="rounded-full p-1 hover:bg-white/20">
<XCircle className="h-6 w-6" />
</button>
</div>
</div>
<div className="p-6">
<p className="text-gray-700 mb-4 leading-7">
اخبر فريق الدعم بما حدث التفاصيل الواضحة تساعدنا على مراجعة هذا الحجز بشكل اسرع
</p>
<textarea
value={message}
onChange={(e) => setMessage(e.target.value)}
placeholder="اكتب تفاصيل البلاغ هنا..."
rows={5}
className="w-full resize-none rounded-xl border border-gray-300 p-3 text-sm focus:outline-none focus:ring-2 focus:ring-red-500"
/>
<div className="mt-5 flex gap-3">
<button
onClick={() => onSubmit(message)}
disabled={submitting}
className={`flex-1 rounded-xl py-2.5 text-sm font-medium transition-colors flex items-center justify-center gap-2 ${
submitting ? 'cursor-not-allowed bg-gray-300 text-gray-500' : 'bg-red-600 text-white hover:bg-red-700'
}`}
>
{submitting ? <Loader2 className="h-4 w-4 animate-spin" /> : <Flag className="h-4 w-4" />}
{submitting ? 'جاري الإرسال...' : 'إرسال البلاغ'}
</button>
<button
onClick={onClose}
disabled={submitting}
className="rounded-xl bg-gray-200 px-4 py-2.5 text-sm font-medium text-gray-700 hover:bg-gray-300 transition-colors disabled:cursor-not-allowed"
>
إلغاء
</button>
</div>
</div>
</motion.div>
</motion.div>
);
}
function ReservationCard({ r, onViewDetails, onPay, onReport, payingId, reportingId }) {
const p = r._prop;
const imgs = propImages(p, r);
const img = imgs.length > 0 ? `${API_BASE}${imgs[0]}` : null;
const addr = propAddr(p, r);
const beds = propBeds(p, r);
const baths = propBaths(p, r);
const isOwnerConfirmed = STATUS_MAP[r.status] === 'ownerConfirmed';
const canRate = STATUS_MAP[r.status] === 'depositPaid' || STATUS_MAP[r.status] === 'completed';
const hasTimeWindow = r.ownerApprovalDate && p?.allowedPaymentPeriod;
const deadline = hasTimeWindow
? new Date(r.ownerApprovalDate).getTime() + parseTimeSpan(p.allowedPaymentPeriod)
: null;
const isExpired = deadline ? Date.now() > deadline : false;
const isPaying = payingId === r.id;
const isReporting = reportingId === r.id;
const [showRating, setShowRating] = useState(false);
const [ratings, setRatings] = useState({ clean: 0, services: 0, ownerBehavior: 0, experience: 0 });
const [ratingComment, setRatingComment] = useState('');
const [submittingRating, setSubmittingRating] = useState(false);
return (
<motion.div initial={{ opacity:0,y:20 }} animate={{ opacity:1,y:0 }}
className="bg-white rounded-2xl shadow-sm hover:shadow-md transition-all border border-gray-200 overflow-hidden">
<div className="p-5">
{img && <div className="mb-4 w-full h-40 rounded-xl overflow-hidden"><img src={img} alt="" className="w-full h-full object-cover" /></div>}
<div className="flex justify-between items-start mb-3">
<div>
<StatusBadge code={r.status} />
{addr && <div className="flex items-center gap-1 text-gray-500 text-sm mt-1"><MapPin className="w-4 h-4"/>{addr}</div>}
</div>
<div className="text-left">
<div className="text-lg font-bold text-amber-600">{r.totalPrice?.toLocaleString() ?? '—'}</div>
<div className="text-xs text-gray-500">السعر الإجمالي</div>
</div>
</div>
{(beds||baths) && <div className="flex gap-3 mb-3 text-sm text-gray-600">{beds>0&&<span>{beds} غرف</span>}{baths>0&&<span>{baths} حمامات</span>}</div>}
<div className="grid grid-cols-2 gap-3 mb-4 text-center">
<div className="bg-gray-50 p-2 rounded-lg">
<Calendar className="w-4 h-4 text-amber-500 mx-auto mb-1"/><div className="text-xs text-gray-500">من</div>
<div className="text-sm font-medium">{new Date(r.startDate).toLocaleDateString('ar')}</div>
</div>
<div className="bg-gray-50 p-2 rounded-lg">
<Calendar className="w-4 h-4 text-amber-500 mx-auto mb-1"/><div className="text-xs text-gray-500">إلى</div>
<div className="text-sm font-medium">{new Date(r.endDate).toLocaleDateString('ar')}</div>
</div>
</div>
{isOwnerConfirmed && hasTimeWindow && <div className="bg-blue-50 p-3 rounded-xl mb-3">
<div className="flex items-center justify-between mb-1">
<span className="text-sm text-blue-800 font-medium flex items-center gap-1"><Timer className="w-4 h-4"/> متبقي للدفع:</span>
<CountdownTimer deadline={deadline} />
</div>
<div className="text-xs text-blue-600">مدة الدفع: {formatWindowDuration(p.allowedPaymentPeriod)}</div>
</div>}
<div className="flex gap-3 pt-3 border-t border-gray-100">
<button onClick={() => onViewDetails(r)}
className="flex-1 bg-gray-100 text-gray-700 py-2 rounded-xl text-sm font-medium hover:bg-gray-200 transition-colors flex items-center justify-center gap-2">
<Eye className="w-4 h-4"/> التفاصيل
</button>
{isOwnerConfirmed && !isExpired && <button onClick={() => onPay(r)} disabled={isPaying}
className={`flex-1 py-2 rounded-xl text-sm font-medium transition-colors flex items-center justify-center gap-2 ${isPaying ? 'bg-gray-300 text-gray-500 cursor-not-allowed' : 'bg-amber-500 text-white hover:bg-amber-600'}`}>
{isPaying ? <Loader2 className="w-4 h-4 animate-spin"/> : <CreditCard className="w-4 h-4"/>} {isPaying ? 'جاري الدفع...' : 'ادفع الآن'}
</button>}
</div>
<button onClick={() => onReport(r)} disabled={isReporting}
className={`w-full mt-3 py-2 rounded-xl text-sm font-medium transition-colors flex items-center justify-center gap-2 ${isReporting ? 'bg-gray-300 text-gray-500 cursor-not-allowed' : 'bg-red-50 text-red-700 hover:bg-red-100'}`}>
{isReporting ? <Loader2 className="w-4 h-4 animate-spin"/> : <Flag className="w-4 h-4"/>} {isReporting ? 'جاري الإبلاغ...' : 'إبلاغ'}
</button>
{canRate && !showRating && <button onClick={() => setShowRating(true)}
className="w-full mt-3 bg-amber-50 text-amber-700 py-2 rounded-xl text-sm font-medium hover:bg-amber-100 transition-colors flex items-center justify-center gap-2">
<Star className="w-4 h-4"/> قيّم هذا العقار
</button>}
{canRate && showRating && <div className="mt-3 bg-amber-50 p-3 rounded-xl">
<div className="space-y-2 mb-3">
{[
{ key: 'clean', label: 'النظافة' },
{ key: 'services', label: 'الخدمات' },
{ key: 'ownerBehavior', label: 'تعامل المالك' },
{ key: 'experience', label: 'التجربة العامة' },
].map(cat => <div key={cat.key} className="flex items-center justify-between">
<span className="text-sm text-gray-700">{cat.label}</span>
<div className="flex gap-0.5">
{[1,2,3,4,5].map(n => (
<button key={n} onClick={() => setRatings(p => ({...p, [cat.key]: n}))}
className={`p-0.5 rounded-full transition-colors ${n <= ratings[cat.key] ? 'text-amber-500' : 'text-gray-300'}`}>
<Star className={`w-4 h-4 ${n <= ratings[cat.key] ? 'fill-amber-500' : ''}`} />
</button>
))}
</div>
</div>)}
</div>
<textarea value={ratingComment} onChange={e => setRatingComment(e.target.value)}
placeholder="أكتب تعليقك (اختياري)"
className="w-full p-2 text-sm border border-amber-200 rounded-lg resize-none focus:outline-none focus:ring-2 focus:ring-amber-500 mb-2" rows={2} />
<div className="flex gap-2">
<button onClick={async () => {
if (!ratings.clean || !ratings.services || !ratings.ownerBehavior || !ratings.experience) return toast.error('قيّم جميع الفئات');
setSubmittingRating(true);
try {
await addPropertyRating({ reservationId: r.id, cleanRating: ratings.clean, servicesRating: ratings.services, ownerBehaviorRating: ratings.ownerBehavior, experienceRating: ratings.experience, comment: ratingComment || null });
toast.success('تم إرسال التقييم');
setShowRating(false);
setRatings({ clean: 0, services: 0, ownerBehavior: 0, experience: 0 });
setRatingComment('');
} catch (e) { toast.error(e?.message || 'فشل إرسال التقييم'); }
finally { setSubmittingRating(false); }
}} disabled={submittingRating}
className="flex-1 bg-amber-500 text-white py-1.5 rounded-lg text-sm font-medium hover:bg-amber-600 transition-colors disabled:bg-gray-300">
{submittingRating ? 'جاري الإرسال...' : 'إرسال التقييم'}
</button>
<button onClick={() => { setShowRating(false); setRatings({ clean: 0, services: 0, ownerBehavior: 0, experience: 0 }); setRatingComment(''); }}
className="px-4 py-1.5 bg-gray-200 text-gray-700 rounded-lg text-sm hover:bg-gray-300 transition-colors">إلغاء</button>
</div>
</div>}
</div>
</motion.div>
);
}
function DetailsModal({ r, isOpen, onClose, onPay, onReport, payingId, reportingId }) {
if (!isOpen || !r) return null;
const p = r._prop;
const isOwnerConfirmed = STATUS_MAP[r.status] === 'ownerConfirmed';
const hasTimeWindow = r.ownerApprovalDate && p?.allowedPaymentPeriod;
const deadline = hasTimeWindow
? new Date(r.ownerApprovalDate).getTime() + parseTimeSpan(p.allowedPaymentPeriod)
: null;
const isExpired = deadline ? Date.now() > deadline : false;
const isPaying = payingId === r.id;
const isReporting = reportingId === r.id;
return (
<motion.div initial={{opacity:0}} animate={{opacity:1}} exit={{opacity:0}}
className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center p-4 z-50" onClick={onClose}>
<motion.div initial={{scale:0.9,y:20}} animate={{scale:1,y:0}} exit={{scale:0.9,y:20}}
className="bg-white rounded-2xl w-full max-w-2xl max-h-[90vh] overflow-y-auto shadow-2xl" onClick={e=>e.stopPropagation()}>
<div className="sticky top-0 bg-gradient-to-r from-amber-500 to-amber-600 p-6 text-white">
<div className="flex justify-between items-center">
<h2 className="text-xl font-bold">تفاصيل الحجز</h2>
<button onClick={onClose} className="p-1 hover:bg-white/20 rounded-full"><XCircle className="w-6 h-6"/></button>
</div>
<p className="text-amber-100 text-sm mt-1">رقم الحجز: #{r.id}</p>
</div>
<div className="p-6 space-y-6">
{p && <div className="bg-gray-50 p-4 rounded-xl">
<h3 className="font-bold text-gray-900 mb-3 flex items-center gap-2"><Home className="w-5 h-5 text-amber-500"/> معلومات العقار</h3>
<p><span className="text-gray-500">العنوان:</span> {propAddr(p, r)||''}</p>
{(propBeds(p, r)||propBaths(p, r)) && <div className="flex gap-3 mt-2">
{propBeds(p, r)>0&&<span className="text-sm bg-white px-2 py-1 rounded-lg">{propBeds(p, r)} غرف</span>}
{propBaths(p, r)>0&&<span className="text-sm bg-white px-2 py-1 rounded-lg">{propBaths(p, r)} حمامات</span>}
</div>}
</div>}
<div className="bg-gray-50 p-4 rounded-xl">
<h3 className="font-bold text-gray-900 mb-3 flex items-center gap-2"><Calendar className="w-5 h-5 text-amber-500"/> تفاصيل الحجز</h3>
<div className="grid grid-cols-2 gap-4">
<div><p className="text-gray-500">تاريخ البداية</p><p className="font-medium">{new Date(r.startDate).toLocaleDateString('ar')}</p></div>
<div><p className="text-gray-500">تاريخ النهاية</p><p className="font-medium">{new Date(r.endDate).toLocaleDateString('ar')}</p></div>
<div><p className="text-gray-500">الحالة</p><StatusBadge code={r.status}/></div>
<div><p className="text-gray-500">تاريخ الإنشاء</p><p className="font-medium">{new Date(r.createdAt).toLocaleDateString('ar')}</p></div>
</div>
</div>
<div className="bg-amber-50 p-4 rounded-xl">
<h3 className="font-bold text-amber-700 mb-3 flex items-center gap-2"><DollarSign className="w-5 h-5"/> المعلومات المالية</h3>
<div className="flex justify-between font-bold"><span className="text-gray-900">الإجمالي</span><span className="text-amber-600 text-lg">{r.totalPrice?.toLocaleString()??''}</span></div>
</div>
{isOwnerConfirmed && hasTimeWindow && <div className="bg-blue-50 p-4 rounded-xl">
<div className="flex items-center justify-between mb-2">
<span className="text-blue-800 font-medium flex items-center gap-2"><Timer className="w-5 h-5"/> متبقي للدفع:</span>
<CountdownTimer deadline={deadline} />
</div>
<div className="text-xs text-blue-600 mb-3">مدة الدفع: {formatWindowDuration(p.allowedPaymentPeriod)}</div>
{!isExpired && <button onClick={() => { onPay(r); onClose(); }} disabled={isPaying}
className={`w-full py-2 rounded-xl font-medium transition-colors flex items-center justify-center gap-2 ${isPaying ? 'bg-gray-300 text-gray-500 cursor-not-allowed' : 'bg-amber-500 text-white hover:bg-amber-600'}`}>
{isPaying ? <Loader2 className="w-5 h-5 animate-spin"/> : <CreditCard className="w-5 h-5"/>} {isPaying ? 'جاري الدفع...' : 'ادفع الآن'}
</button>}
</div>}
<button onClick={() => { onReport(r); onClose(); }} disabled={isReporting}
className={`w-full py-2 rounded-xl font-medium transition-colors flex items-center justify-center gap-2 ${isReporting ? 'bg-gray-300 text-gray-500 cursor-not-allowed' : 'bg-red-50 text-red-700 hover:bg-red-100'}`}>
{isReporting ? <Loader2 className="w-5 h-5 animate-spin"/> : <Flag className="w-5 h-5"/>} {isReporting ? 'جاري الإبلاغ...' : 'إبلاغ'}
</button>
</div>
</motion.div>
</motion.div>
);
}
export default function UserReservationsPage() {
const router = useRouter();
const [reservations, setReservations] = useState([]);
const [filtered, setFiltered] = useState([]);
const [loading, setLoading] = useState(true);
const [selected, setSelected] = useState(null);
const [filterStatus, setFilterStatus] = useState('all');
const [searchTerm, setSearchTerm] = useState('');
const [payingId, setPayingId] = useState(null);
const [reportDialog, setReportDialog] = useState({ open: false, reservation: null });
const [reportingId, setReportingId] = useState(null);
const [selectedPayment, setSelectedPayment] = useState('cash');
const [paymentDialog, setPaymentDialog] = useState({ open: false, reservation: null });
useEffect(() => { if (!AuthService.getUser()) { router.push('/login'); return; } loadReservations(); }, [router]);
const loadReservations = useCallback(async () => {
try {
const [data, rentProps] = await Promise.all([
getUserReservations(),
getRentProperties().catch(() => []),
]);
const list = Array.isArray(data) ? data : [];
const propsList = Array.isArray(rentProps) ? rentProps : [];
const propMap = {};
propsList.forEach(rp => {
const info = rp?.propertyInformation ?? {};
if (rp?.allowedPaymentPeriod) info.allowedPaymentPeriod = rp.allowedPaymentPeriod;
propMap[rp.propertyInformationId] = info;
propMap[rp.propertyInformation?.id] = info;
});
const enriched = list.map(r => {
if (r.propertyId && propMap[r.propertyId]) r._prop = propMap[r.propertyId];
return r;
});
setReservations(enriched);
setFiltered(enriched);
} catch (err) {
console.error(err);
toast.error('فشل تحميل الحجوزات');
setReservations([]);
setFiltered([]);
}
setLoading(false);
}, []);
useEffect(() => {
let r = reservations;
if (filterStatus !== 'all') r = r.filter(x => STATUS_MAP[x.status] === filterStatus);
if (searchTerm) {
const q = searchTerm.toLowerCase();
r = r.filter(x => propAddr(x._prop, x).toLowerCase().includes(q) || String(x.id).includes(q));
}
setFiltered(r);
}, [reservations, filterStatus, searchTerm]);
const allStatuses = [...new Set(reservations.map(r => STATUS_MAP[r.status]))];
const counts = { all: reservations.length, ...Object.fromEntries(allStatuses.map(s => [s, reservations.filter(r => STATUS_MAP[r.status] === s).length])) };
const openPaymentDialog = (r) => {
setPaymentDialog({ open: true, reservation: r });
};
const closePaymentDialog = () => {
setPaymentDialog({ open: false, reservation: null });
};
const handleConfirmPay = async (r) => {
setPayingId(r.id);
try {
await payDeposit({
reservationId: r.id,
paymentTypeId: 1,
transactionType: 1,
comment: null,
});
toast.success('تم دفع السلفة بنجاح!');
closePaymentDialog();
loadReservations();
} catch (err) {
toast.error(err?.message || 'فشل عملية الدفع');
} finally {
setPayingId(null);
}
};
const openReportDialog = (r) => {
setReportDialog({ open: true, reservation: r });
};
const closeReportDialog = () => {
setReportDialog({ open: false, reservation: null });
};
const handleSubmitReport = async (message) => {
if (!reportDialog.reservation) return;
setReportingId(reportDialog.reservation.id);
try {
await reportReservation(reportDialog.reservation.id, message.trim() || null);
toast.success('تم إرسال البلاغ بنجاح');
closeReportDialog();
} catch (err) {
toast.error(err?.message || 'فشل إرسال البلاغ');
} finally {
setReportingId(null);
}
};
if (loading) return <div className="min-h-screen bg-gray-50 flex items-center justify-center"><Loader2 className="w-12 h-12 text-amber-500 animate-spin"/></div>;
return (
<div className="min-h-screen bg-gray-50 py-8" dir="rtl">
<Toaster position="top-center" reverseOrder={false} />
<DetailsModal
r={selected}
isOpen={!!selected}
onClose={() => setSelected(null)}
onPay={openPaymentDialog}
onReport={openReportDialog}
payingId={payingId}
reportingId={reportingId}
/>
<ReportDialog
isOpen={reportDialog.open}
reservation={reportDialog.reservation}
onClose={closeReportDialog}
onSubmit={handleSubmitReport}
submitting={!!reportingId}
/>
<PaymentDialog
isOpen={paymentDialog.open}
reservation={paymentDialog.reservation}
onClose={closePaymentDialog}
selectedPayment={selectedPayment}
onSelectPayment={setSelectedPayment}
onConfirmPay={handleConfirmPay}
payingId={payingId}
/>
<div className="container mx-auto px-4">
<motion.div initial={{opacity:0,y:-20}} animate={{opacity:1,y:0}} className="mb-8">
<button onClick={() => router.back()} className="flex items-center gap-2 text-gray-600 hover:text-amber-600 mb-4"><ArrowLeft className="w-5 h-5"/> الرجوع</button>
<h1 className="text-3xl font-bold text-gray-900 mb-2">حجوزاتي</h1>
<p className="text-gray-600">لديك {reservations.length} حجز</p>
</motion.div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8">
{Object.entries(counts).map(([s, c]) => (
<motion.div key={s} initial={{opacity:0,y:20}} animate={{opacity:1,y:0}}
className={`bg-white rounded-xl shadow-sm p-4 text-center border cursor-pointer hover:shadow-md transition-all ${filterStatus===s?'border-amber-500 bg-amber-50':'border-gray-200'}`}
onClick={() => setFilterStatus(s)}>
<div className="text-2xl font-bold text-amber-600">{c}</div>
<div className="text-sm text-gray-600">{s==='all'?'الكل':(STATUS_UI[s]?.label||s)}</div>
</motion.div>
))}
</div>
<div className="mb-6 relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400"/>
<input type="text" placeholder="ابحث بعنوان العقار أو رقم الحجز..." value={searchTerm} onChange={e=>setSearchTerm(e.target.value)}
className="w-full pl-10 pr-4 py-3 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500"/>
</div>
{filtered.length === 0 ? (
<div className="bg-white rounded-2xl p-12 text-center border-2 border-dashed border-gray-300">
<Calendar className="w-12 h-12 text-amber-600 mx-auto mb-4"/>
<h3 className="text-xl font-bold text-gray-900 mb-2">لا توجد حجوزات</h3>
<p className="text-gray-600">لم تقم بأي حجز حتى الآن</p>
</div>
) : (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{filtered.map(r => (
<ReservationCard
key={r.id}
r={r}
onViewDetails={setSelected}
onPay={openPaymentDialog}
onReport={openReportDialog}
payingId={payingId}
reportingId={reportingId}
/>
))}
</div>
)}
</div>
</div>
);
}