diff --git a/app/owner/reservations/page.js b/app/owner/reservations/page.js index b8a4697..43fd68a 100644 --- a/app/owner/reservations/page.js +++ b/app/owner/reservations/page.js @@ -1,104 +1,521 @@ +// '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, User, RefreshCw, Mail, Phone, +// } from 'lucide-react'; +// import toast, { Toaster } from 'react-hot-toast'; +// import AuthService from '../../services/AuthService'; +// import { getRentProperty } from '../../utils/api'; + +// 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-green-100 text-green-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-blue-100 text-blue-800', icon: CheckCircle }, +// cancelled: { label: 'ملغي', color: 'bg-gray-100 text-gray-800', icon: XCircle }, +// }; + +// const sLabel = c => STATUS_UI[STATUS_MAP[c]]?.label ?? String(c); +// const sColor = c => STATUS_UI[STATUS_MAP[c]]?.color ?? 'bg-gray-100 text-gray-700'; +// const sIcon = c => STATUS_UI[STATUS_MAP[c]]?.icon ?? Clock; + +// function StatusBadge({ code }) { +// const Icon = sIcon(code); +// return {sLabel(code)}; +// } + +// async function enrich(r) { +// if (!r.propertyId) return r; +// try { +// const prop = await getRentProperty(r.propertyId); +// r._prop = prop?.propertyInformation ?? prop ?? null; +// } catch { /* skip */ } +// return r; +// } + +// const pAddr = p => p?.address ?? ''; +// const pImgs = p => Array.isArray(p?.images) ? p.images : []; +// const pBeds = p => p?.numberOfBedRooms ?? 0; +// const pBaths = p => p?.numberOfBathRooms ?? 0; +// const API = (token, method, path, body) => fetch(`${API_BASE}${path}`, { +// method: method || 'GET', +// headers: { 'Content-Type': 'application/json', ...(token && { Authorization: `Bearer ${token}` }) }, +// ...(body && { body: JSON.stringify(body) }), +// }); + +// function OwnerCard({ r, onViewDetails, onConfirm, onReject }) { +// const p = r._prop; +// const imgs = pImgs(p); +// const img = imgs.length > 0 ? `${API_BASE}${imgs[0]}` : null; +// const addr = pAddr(p); +// const isPending = r.status === 0; // Pending + +// return ( +// +//
+// {img &&
} +//
+//
+// +// {addr &&
{addr}
} +//
+//
+//
{r.totalPrice?.toLocaleString()??'—'}
+//
السعر الإجمالي
+//
+//
+// {(pBeds(p)||pBaths(p)) &&
{pBeds(p)>0&&{pBeds(p)} غرف}{pBaths(p)>0&&{pBaths(p)} حمامات}
} +//
+//
+//
من
+//
{new Date(r.startDate).toLocaleDateString('ar')}
+//
+//
+//
إلى
+//
{new Date(r.endDate).toLocaleDateString('ar')}
+//
+//
+//
+// +// {isPending && <> +// +// +// } +//
+//
+//
+// ); +// } + +// function DetailsModal({ r, isOpen, onClose }) { +// if (!isOpen || !r) return null; +// const p = r._prop; +// return ( +// +// e.stopPropagation()}> +//
+//
+//

طلب حجز #{r.id}

+// +//
+//
+//
+// {p &&
+//

معلومات العقار

+//

العنوان: {pAddr(p)||'—'}

+// {(pBeds(p)||pBaths(p)) &&
+// {pBeds(p)>0&&{pBeds(p)} غرف} +// {pBaths(p)>0&&{pBaths(p)} حمامات} +//
} +//
} +//
+//

تفاصيل الحجز

+//
+//

تاريخ البداية

{new Date(r.startDate).toLocaleDateString('ar')}

+//

تاريخ النهاية

{new Date(r.endDate).toLocaleDateString('ar')}

+//

الحالة

+//

تاريخ الإنشاء

{new Date(r.createdAt).toLocaleDateString('ar')}

+//
+//
+//
+//

المعلومات المالية

+//
الإجمالي{r.totalPrice?.toLocaleString()??'—'}
+//
+//
+//
+//
+// ); +// } + +// export default function OwnerReservationRequestsPage() { +// 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(''); + +// useEffect(() => { +// if (!AuthService.getUser() || !AuthService.isOwner()) { router.push('/auth/choose-role'); return; } +// loadReservations(); +// }, [router]); + +// const loadReservations = useCallback(async () => { +// try { +// const token = AuthService.getToken(); +// const res = await fetch(`${API_BASE}/Reservations/GetOwnerResevationRequests`, { +// headers: { Authorization: `Bearer ${token}` }, +// }); +// if (!res.ok) throw new Error(`HTTP ${res.status}`); +// const json = await res.json(); +// let list = json.data || json || []; +// if (!Array.isArray(list)) list = []; +// const enriched = await Promise.all(list.map(enrich)); +// setReservations(enriched); +// setFiltered(enriched); +// } catch (err) { +// console.error(err); +// toast.error('فشل تحميل طلبات الحجز'); +// } +// 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 => pAddr(x._prop).toLowerCase().includes(q) || String(x.id).includes(q)); +// } +// setFiltered(r); +// }, [reservations, filterStatus, searchTerm]); + +// const handleConfirm = async (r) => { +// try { +// const res = await API(AuthService.getToken(), 'PUT', `/Reservations/owner-confirm/${r.id}`); +// if (!res.ok) throw new Error(`HTTP ${res.status}`); +// toast.success('تم قبول الحجز بنجاح'); +// await loadReservations(); +// } catch (err) { console.error(err); toast.error('فشل قبول الحجز'); } +// }; + +// const handleReject = async (r) => { +// if (!confirm('هل أنت متأكد من رفض هذا الحجز؟')) return; +// try { +// const res = await API(AuthService.getToken(), 'PUT', `/Reservations/reject/${r.id}`); +// if (!res.ok) throw new Error(`HTTP ${res.status}`); +// toast.success('تم رفض الحجز'); +// await loadReservations(); +// } catch (err) { console.error(err); toast.error('فشل رفض الحجز'); } +// }; + +// 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])) }; + +// if (loading) return
; + +// return ( +//
+// +// setSelected(null)} /> +//
+// +// +//
+//
+//

طلبات الحجز

+//

لديك {reservations.length} طلب

+//
+// +//
+//
+//
+// {Object.entries(counts).map(([s, c]) => ( +// setFilterStatus(s)}> +//
{c}
+//
{s==='all'?'الكل':(STATUS_UI[s]?.label||s)}
+//
+// ))} +//
+//
+// +// 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"/> +//
+// {filtered.length === 0 ? ( +//
+// +//

لا توجد طلبات

+//

لم يتم استلام أي طلبات حجز حتى الآن

+//
+// ) : ( +//
+// {filtered.map(r => )} +//
+// )} +//
+//
+// ); +// } + + '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, User, RefreshCw, Mail, Phone, + Calendar, + Clock, + CheckCircle, + XCircle, + Eye, + Loader2, + Search, + MapPin, + DollarSign, + Home, + ArrowLeft, + RefreshCw, } from 'lucide-react'; import toast, { Toaster } from 'react-hot-toast'; import AuthService from '../../services/AuthService'; import { getRentProperty } from '../../utils/api'; -const API_BASE = process.env.NEXT_PUBLIC_API_URL || 'https://45.93.137.91.nip.io/api'; +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_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-green-100 text-green-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-blue-100 text-blue-800', icon: CheckCircle }, - cancelled: { label: 'ملغي', color: 'bg-gray-100 text-gray-800', icon: XCircle }, + pending: { + label: 'قيد الانتظار', + color: 'bg-yellow-100 text-yellow-800', + icon: Clock, + }, + ownerConfirmed: { + label: 'مؤكد', + color: 'bg-green-100 text-green-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-blue-100 text-blue-800', + icon: CheckCircle, + }, + cancelled: { + label: 'ملغي', + color: 'bg-gray-100 text-gray-800', + icon: XCircle, + }, }; -const sLabel = c => STATUS_UI[STATUS_MAP[c]]?.label ?? String(c); -const sColor = c => STATUS_UI[STATUS_MAP[c]]?.color ?? 'bg-gray-100 text-gray-700'; -const sIcon = c => STATUS_UI[STATUS_MAP[c]]?.icon ?? Clock; +const getStatusKey = (code) => STATUS_MAP[Number(code)] || 'pending'; + +const sLabel = (code) => STATUS_UI[getStatusKey(code)]?.label ?? String(code); + +const sColor = (code) => + STATUS_UI[getStatusKey(code)]?.color ?? 'bg-gray-100 text-gray-700'; + +const sIcon = (code) => STATUS_UI[getStatusKey(code)]?.icon ?? Clock; function StatusBadge({ code }) { const Icon = sIcon(code); - return {sLabel(code)}; + + return ( + + {sLabel(code)} + + ); } async function enrich(r) { - if (!r.propertyId) return r; + if (!r?.propertyId) return r; + try { const prop = await getRentProperty(r.propertyId); - r._prop = prop?.propertyInformation ?? prop ?? null; - } catch { /* skip */ } - return r; + + return { + ...r, + _prop: prop?.propertyInformation ?? prop ?? null, + }; + } catch { + return { + ...r, + _prop: null, + }; + } } -const pAddr = p => p?.address ?? ''; -const pImgs = p => Array.isArray(p?.images) ? p.images : []; -const pBeds = p => p?.numberOfBedRooms ?? 0; -const pBaths = p => p?.numberOfBathRooms ?? 0; -const API = (token, method, path, body) => fetch(`${API_BASE}${path}`, { - method: method || 'GET', - headers: { 'Content-Type': 'application/json', ...(token && { Authorization: `Bearer ${token}` }) }, - ...(body && { body: JSON.stringify(body) }), -}); +const pAddr = (p) => p?.address ?? ''; +const pImgs = (p) => (Array.isArray(p?.images) ? p.images : []); +const pBeds = (p) => p?.numberOfBedRooms ?? 0; +const pBaths = (p) => p?.numberOfBathRooms ?? 0; -function OwnerCard({ r, onViewDetails, onConfirm, onReject }) { +const API = (token, method, path, body) => { + return fetch(`${API_BASE}${path}`, { + method: method || 'GET', + headers: { + 'Content-Type': 'application/json', + ...(token && { Authorization: `Bearer ${token}` }), + }, + ...(body && { body: JSON.stringify(body) }), + }); +}; + +function buildImageUrl(path) { + if (!path) return null; + + if (path.startsWith('http://') || path.startsWith('https://')) { + return path; + } + + return `${API_BASE}${path}`; +} + +function OwnerCard({ + r, + onViewDetails, + onConfirm, + onReject, + actionLoadingId, +}) { const p = r._prop; const imgs = pImgs(p); - const img = imgs.length > 0 ? `${API_BASE}${imgs[0]}` : null; + const img = imgs.length > 0 ? buildImageUrl(imgs[0]) : null; const addr = pAddr(p); - const isPending = r.status === 0; // Pending + const isPending = Number(r.status) === 0; + const isActionLoading = actionLoadingId === r.id; return ( - +
- {img &&
} + {img && ( +
+ +
+ )} +
- - {addr &&
{addr}
} + + + {addr && ( +
+ + {addr} +
+ )}
+
-
{r.totalPrice?.toLocaleString()??'—'}
+
+ {r.totalPrice?.toLocaleString() ?? '—'} +
السعر الإجمالي
- {(pBeds(p)||pBaths(p)) &&
{pBeds(p)>0&&{pBeds(p)} غرف}{pBaths(p)>0&&{pBaths(p)} حمامات}
} + + {(pBeds(p) || pBaths(p)) && ( +
+ {pBeds(p) > 0 && {pBeds(p)} غرف} + {pBaths(p) > 0 && {pBaths(p)} حمامات} +
+ )} +
-
من
-
{new Date(r.startDate).toLocaleDateString('ar')}
+ +
من
+
+ {r.startDate + ? new Date(r.startDate).toLocaleDateString('ar') + : '—'} +
+
-
إلى
-
{new Date(r.endDate).toLocaleDateString('ar')}
+ +
إلى
+
+ {r.endDate ? new Date(r.endDate).toLocaleDateString('ar') : '—'} +
-
- - {isPending && <> - - - } + + {isPending && ( + <> + + + + + )}
@@ -107,39 +524,120 @@ function OwnerCard({ r, onViewDetails, onConfirm, onReject }) { function DetailsModal({ r, isOpen, onClose }) { if (!isOpen || !r) return null; + const p = r._prop; + return ( - - e.stopPropagation()}> + + e.stopPropagation()} + >

طلب حجز #{r.id}

- + +
+
- {p &&
-

معلومات العقار

-

العنوان: {pAddr(p)||'—'}

- {(pBeds(p)||pBaths(p)) &&
- {pBeds(p)>0&&{pBeds(p)} غرف} - {pBaths(p)>0&&{pBaths(p)} حمامات} -
} -
} + {p && ( +
+

+ + معلومات العقار +

+ +

+ العنوان:{' '} + {pAddr(p) || '—'} +

+ + {(pBeds(p) || pBaths(p)) && ( +
+ {pBeds(p) > 0 && ( + + {pBeds(p)} غرف + + )} + + {pBaths(p) > 0 && ( + + {pBaths(p)} حمامات + + )} +
+ )} +
+ )} +
-

تفاصيل الحجز

+

+ + تفاصيل الحجز +

+
-

تاريخ البداية

{new Date(r.startDate).toLocaleDateString('ar')}

-

تاريخ النهاية

{new Date(r.endDate).toLocaleDateString('ar')}

-

الحالة

-

تاريخ الإنشاء

{new Date(r.createdAt).toLocaleDateString('ar')}

+
+

تاريخ البداية

+

+ {r.startDate + ? new Date(r.startDate).toLocaleDateString('ar') + : '—'} +

+
+ +
+

تاريخ النهاية

+

+ {r.endDate ? new Date(r.endDate).toLocaleDateString('ar') : '—'} +

+
+ +
+

الحالة

+ +
+ +
+

تاريخ الإنشاء

+

+ {r.createdAt + ? new Date(r.createdAt).toLocaleDateString('ar') + : '—'} +

+
+
-

المعلومات المالية

-
الإجمالي{r.totalPrice?.toLocaleString()??'—'}
+

+ + المعلومات المالية +

+ +
+ الإجمالي + + {r.totalPrice?.toLocaleString() ?? '—'} + +
@@ -149,114 +647,259 @@ function DetailsModal({ r, isOpen, onClose }) { export default function OwnerReservationRequestsPage() { 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(''); - - useEffect(() => { - if (!AuthService.getUser() || !AuthService.isOwner()) { router.push('/auth/choose-role'); return; } - loadReservations(); - }, [router]); + const [actionLoadingId, setActionLoadingId] = useState(null); const loadReservations = useCallback(async () => { try { + setLoading(true); + const token = AuthService.getToken(); - const res = await fetch(`${API_BASE}/Reservations/GetOwnerResevationRequests`, { - headers: { Authorization: `Bearer ${token}` }, - }); - if (!res.ok) throw new Error(`HTTP ${res.status}`); + + const res = await fetch( + `${API_BASE}/Reservations/GetOwnerResevationRequests`, + { + method: 'GET', + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + } + ); + + if (!res.ok) { + const errorText = await res.text(); + throw new Error(errorText || `HTTP ${res.status}`); + } + const json = await res.json(); + let list = json.data || json || []; if (!Array.isArray(list)) list = []; + const enriched = await Promise.all(list.map(enrich)); + setReservations(enriched); setFiltered(enriched); } catch (err) { console.error(err); toast.error('فشل تحميل طلبات الحجز'); + setReservations([]); + setFiltered([]); + } finally { + setLoading(false); } - 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 => pAddr(x._prop).toLowerCase().includes(q) || String(x.id).includes(q)); + if (!AuthService.getUser() || !AuthService.isOwner()) { + router.push('/auth/choose-role'); + return; } - setFiltered(r); + + loadReservations(); + }, [router, loadReservations]); + + useEffect(() => { + let result = reservations; + + if (filterStatus !== 'all') { + result = result.filter((x) => getStatusKey(x.status) === filterStatus); + } + + if (searchTerm.trim()) { + const q = searchTerm.trim().toLowerCase(); + + result = result.filter( + (x) => + pAddr(x._prop).toLowerCase().includes(q) || String(x.id).includes(q) + ); + } + + setFiltered(result); }, [reservations, filterStatus, searchTerm]); const handleConfirm = async (r) => { try { - const res = await API(AuthService.getToken(), 'PUT', `/Reservations/owner-confirm/${r.id}`); - if (!res.ok) throw new Error(`HTTP ${res.status}`); + setActionLoadingId(r.id); + + const res = await API( + AuthService.getToken(), + 'PUT', + `/Reservations/OwnerConfirmReservation/owner-confirm/${r.id}` + ); + + if (!res.ok) { + const errorText = await res.text(); + throw new Error(errorText || `HTTP ${res.status}`); + } + toast.success('تم قبول الحجز بنجاح'); await loadReservations(); - } catch (err) { console.error(err); toast.error('فشل قبول الحجز'); } + } catch (err) { + console.error(err); + toast.error('فشل قبول الحجز'); + } finally { + setActionLoadingId(null); + } }; const handleReject = async (r) => { if (!confirm('هل أنت متأكد من رفض هذا الحجز؟')) return; + try { - const res = await API(AuthService.getToken(), 'PUT', `/Reservations/reject/${r.id}`); - if (!res.ok) throw new Error(`HTTP ${res.status}`); + setActionLoadingId(r.id); + + const res = await API( + AuthService.getToken(), + 'POST', + `/Reservations/ChangeReservationStatus?id=${r.id}&newStatus=5` + ); + + if (!res.ok) { + const errorText = await res.text(); + throw new Error(errorText || `HTTP ${res.status}`); + } + toast.success('تم رفض الحجز'); await loadReservations(); - } catch (err) { console.error(err); toast.error('فشل رفض الحجز'); } + } catch (err) { + console.error(err); + toast.error('فشل رفض الحجز'); + } finally { + setActionLoadingId(null); + } }; - 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 allStatuses = [ + ...new Set(reservations.map((r) => getStatusKey(r.status)).filter(Boolean)), + ]; - if (loading) return
; + const counts = { + all: reservations.length, + ...Object.fromEntries( + allStatuses.map((s) => [ + s, + reservations.filter((r) => getStatusKey(r.status) === s).length, + ]) + ), + }; + + if (loading) { + return ( +
+ +
+ ); + } return (
- setSelected(null)} /> + + setSelected(null)} + /> +
- - + + +
-

طلبات الحجز

+

+ طلبات الحجز +

لديك {reservations.length} طلب

- + +
+
{Object.entries(counts).map(([s, c]) => ( - setFilterStatus(s)}> + setFilterStatus(s)} + >
{c}
-
{s==='all'?'الكل':(STATUS_UI[s]?.label||s)}
+
+ {s === 'all' ? 'الكل' : STATUS_UI[s]?.label || s} +
))}
+
- - 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"/> + + + 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" + />
+ {filtered.length === 0 ? (
- -

لا توجد طلبات

-

لم يتم استلام أي طلبات حجز حتى الآن

+ +

+ لا توجد طلبات +

+

+ لم يتم استلام أي طلبات حجز حتى الآن +

) : (
- {filtered.map(r => )} + {filtered.map((r) => ( + + ))}
)}
); -} +} \ No newline at end of file diff --git a/app/reservations/page.js b/app/reservations/page.js index 29c1074..5c8eb52 100644 --- a/app/reservations/page.js +++ b/app/reservations/page.js @@ -223,4 +223,4 @@ export default function UserReservationsPage() { ); -} +} \ No newline at end of file