diff --git a/app/owner/reservations/page.js b/app/owner/reservations/page.js index 1af5ac4..b8a4697 100644 --- a/app/owner/reservations/page.js +++ b/app/owner/reservations/page.js @@ -4,21 +4,8 @@ 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, User, RefreshCw, Mail, Phone, } from 'lucide-react'; import toast, { Toaster } from 'react-hot-toast'; import AuthService from '../../services/AuthService'; @@ -26,212 +13,133 @@ import { getRentProperty } from '../../utils/api'; const API_BASE = process.env.NEXT_PUBLIC_API_URL || 'https://45.93.137.91.nip.io/api'; -const statusConfig = { +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 }, - rejected: { label: 'مرفوض', color: 'bg-red-100 text-red-800', icon: XCircle }, + completed: { label: 'منتهي', color: 'bg-blue-100 text-blue-800', icon: CheckCircle }, cancelled: { label: 'ملغي', color: 'bg-gray-100 text-gray-800', icon: XCircle }, - completed: { label: 'منتهي', color: 'bg-blue-100 text-blue-800', icon: CheckCircle }, }; -function StatusBadge({ status }) { - const config = statusConfig[status] || { label: status, color: 'bg-gray-100 text-gray-700', icon: Clock }; - const Icon = config.icon; - return ( - - - {config.label} - - ); +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)}; } -function OwnerReservationCard({ reservation, onViewDetails, onConfirm, onReject }) { - const property = reservation.property || reservation.propertyInformation; - const user = reservation.user; - const isPending = reservation.status === 'pending'; +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 &&
} +
- - {property && ( -
- - {property.address || 'عقار'} -
- )} + + {addr &&
{addr}
}
-
- {reservation.totalPrice?.toLocaleString() || '—'} -
+
{r.totalPrice?.toLocaleString()??'—'}
السعر الإجمالي
- - {user && ( -
-
-
- -
-
-

{user.name || user.email || 'مستخدم'}

-
- {user.phoneNumber && <>{user.phoneNumber}} - {user.email && <>{user.email}} -
-
-
-
- )} - + {(pBeds(p)||pBaths(p)) &&
{pBeds(p)>0&&{pBeds(p)} غرف}{pBaths(p)>0&&{pBaths(p)} حمامات}
}
- -
من
-
- {new Date(reservation.startDate).toLocaleDateString('ar')} -
+
من
+
{new Date(r.startDate).toLocaleDateString('ar')}
- -
إلى
-
- {new Date(reservation.endDate).toLocaleDateString('ar')} -
+
إلى
+
{new Date(r.endDate).toLocaleDateString('ar')}
- -
- - - {isPending && ( - <> - - - - )} + {isPending && <> + + + }
); } -function DetailsModal({ reservation, isOpen, onClose }) { - if (!isOpen || !reservation) return null; - const property = reservation.property || reservation.propertyInformation; - const user = reservation.user; - +function DetailsModal({ r, isOpen, onClose }) { + if (!isOpen || !r) return null; + const p = r._prop; return ( - - e.stopPropagation()} - > + + e.stopPropagation()}>
-

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

- +

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

+
-
- {user && ( -
-

- معلومات المستأجر -

-

الاسم: {user.name || '—'}

-

البريد: {user.email || '—'}

- {user.phoneNumber &&

الهاتف: {user.phoneNumber}

} -
- )} - - {property && ( -
-

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

-

العنوان: {property.address || '—'}

- {property.numberOfBedRooms && ( -
- {property.numberOfBedRooms} غرف - {property.numberOfBathRooms} حمامات -
- )} -
- )} - + {p &&
+

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

+

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

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

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

+

تفاصيل الحجز

-
-

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

-

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

-
-
-

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

-

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

-
-
-

الحالة

- -
-
-

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

-

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

-
+

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

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

+

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

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

+

الحالة

+

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

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

-
-

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

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

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

+
الإجمالي{r.totalPrice?.toLocaleString()??'—'}
@@ -249,182 +157,103 @@ export default function OwnerReservationRequestsPage() { const [searchTerm, setSearchTerm] = useState(''); useEffect(() => { - const user = AuthService.getUser(); - if (!user || !AuthService.isOwner()) { - router.push('/auth/choose-role'); - return; - } + if (!AuthService.getUser() || !AuthService.isOwner()) { router.push('/auth/choose-role'); return; } loadReservations(); }, [router]); - const loadReservations = async () => { + 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 = []; - - // Enrich each reservation with property details - const enriched = await Promise.all( - list.map(async (r) => { - if (!r.property && r.propertyId) { - try { - const propRes = await getRentProperty(r.propertyId); - r.property = propRes?.data?.propertyInformation || propRes?.data || propRes || null; - } catch (e) { - console.warn('Failed to load property', r.propertyId); - } - } - return r; - }) - ); - + const enriched = await Promise.all(list.map(enrich)); setReservations(enriched); setFiltered(enriched); } catch (err) { - console.error('Failed to load owner reservations:', err); + console.error(err); toast.error('فشل تحميل طلبات الحجز'); } setLoading(false); - }; + }, []); useEffect(() => { - let result = reservations; - if (filterStatus !== 'all') result = result.filter(r => r.status === filterStatus); + let r = reservations; + if (filterStatus !== 'all') r = r.filter(x => STATUS_MAP[x.status] === filterStatus); if (searchTerm) { const q = searchTerm.toLowerCase(); - result = result.filter(r => { - const addr = (r.property?.address || '').toLowerCase(); - const userName = ((r.user?.name) || (r.user?.email) || '').toLowerCase(); - const sid = String(r.id).toLowerCase(); - return addr.includes(q) || userName.includes(q) || sid.includes(q); - }); + r = r.filter(x => pAddr(x._prop).toLowerCase().includes(q) || String(x.id).includes(q)); } - setFiltered(result); + setFiltered(r); }, [reservations, filterStatus, searchTerm]); - const handleConfirm = async (reservation) => { + const handleConfirm = async (r) => { try { - const token = AuthService.getToken(); - const id = typeof reservation.id === 'number' ? reservation.id : parseInt(reservation.id); - const res = await fetch(`${API_BASE}/Reservations/owner-confirm/${id}`, { - method: 'PUT', - headers: { Authorization: `Bearer ${token}` }, - }); - + 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('Failed to confirm:', err); - toast.error('فشل قبول الحجز'); - } + } catch (err) { console.error(err); toast.error('فشل قبول الحجز'); } }; - const handleReject = async (reservation) => { + const handleReject = async (r) => { if (!confirm('هل أنت متأكد من رفض هذا الحجز؟')) return; try { - const token = AuthService.getToken(); - const id = typeof reservation.id === 'number' ? reservation.id : parseInt(reservation.id); - const res = await fetch(`${API_BASE}/Reservations/reject/${id}`, { - method: 'PUT', - headers: { Authorization: `Bearer ${token}` }, - }); - + 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('Failed to reject:', err); - toast.error('فشل رفض الحجز'); - } + } catch (err) { console.error(err); toast.error('فشل رفض الحجز'); } }; - const statuses = ['all', ...new Set(reservations.map(r => r.status))]; - const statusCounts = { - all: reservations.length, - ...Object.fromEntries( - [...new Set(reservations.map(r => r.status))].map(s => [s, reservations.filter(r => r.status === s).length]) - ), - }; + 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
; + if (loading) return
; return (
- setSelected(null)} /> - + setSelected(null)} />
- - + +

طلبات الحجز

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

- +
-
- {Object.entries(statusCounts).map(([status, count]) => ( - setFilterStatus(status)} - > -
{count}
-
{status === 'all' ? 'الكل' : (statusConfig[status]?.label || status)}
+ {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" - /> + + 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 => )}
)}
diff --git a/app/reservations/page.js b/app/reservations/page.js index 7686e65..29c1074 100644 --- a/app/reservations/page.js +++ b/app/reservations/page.js @@ -4,18 +4,8 @@ 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, - Building2 + Calendar, Clock, CheckCircle, XCircle, Eye, Loader2, Search, + MapPin, DollarSign, Home, ArrowLeft, } from 'lucide-react'; import toast, { Toaster } from 'react-hot-toast'; import AuthService from '../services/AuthService'; @@ -23,91 +13,82 @@ import { getRentProperty } from '../utils/api'; const API_BASE = process.env.NEXT_PUBLIC_API_URL || 'https://45.93.137.91.nip.io/api'; -const statusConfig = { - 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 }, +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 }, - rejected: { label: 'مرفوض', color: 'bg-red-100 text-red-800', icon: XCircle }, - cancelled: { label: 'ملغي', color: 'bg-gray-100 text-gray-800', icon: XCircle }, - completed: { 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 StatusBadge({ status }) { - const config = statusConfig[status] || { label: status, color: 'bg-gray-100 text-gray-700', icon: Clock }; - const Icon = config.icon; +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 ( - - - {config.label} + + {statusLabel(code)} ); } -function ReservationCard({ reservation, onViewDetails }) { - const property = reservation.property || reservation.propertyInformation; +async function enrich(reservation) { + if (!reservation.propertyId) return reservation; + try { + const prop = await getRentProperty(reservation.propertyId); + reservation._prop = prop?.propertyInformation ?? prop ?? null; + } catch { /* skip */ } + return reservation; +} + +const propAddr = (p) => p?.address ?? ''; +const propImages = (p) => Array.isArray(p?.images) ? p.images : []; +const propBeds = (p) => p?.numberOfBedRooms ?? 0; +const propBaths = (p) => p?.numberOfBathRooms ?? 0; + +function ReservationCard({ r, onViewDetails }) { + const p = r._prop; + const imgs = propImages(p); + const img = imgs.length > 0 ? `${API_BASE}${imgs[0]}` : null; + const addr = propAddr(p); + const beds = propBeds(p); + const baths = propBaths(p); return ( - +
-
+ {img &&
} +
-
- -
- {property && ( -
- - {property.address || 'عقار'} -
- )} + + {addr &&
{addr}
}
-
- {reservation.totalPrice?.toLocaleString() || '—'} -
+
{r.totalPrice?.toLocaleString() ?? '—'}
السعر الإجمالي
- - {property?.images?.length > 0 && ( -
- -
- )} - + {(beds||baths) &&
{beds>0&&{beds} غرف}{baths>0&&{baths} حمامات}
}
- -
من
-
- {new Date(reservation.startDate).toLocaleDateString('ar')} -
+
من
+
{new Date(r.startDate).toLocaleDateString('ar')}
- -
إلى
-
- {new Date(reservation.endDate).toLocaleDateString('ar')} -
+
إلى
+
{new Date(r.endDate).toLocaleDateString('ar')}
-
-
@@ -115,83 +96,43 @@ function ReservationCard({ reservation, onViewDetails }) { ); } -function DetailsModal({ reservation, isOpen, onClose }) { - if (!isOpen || !reservation) return null; - const property = reservation.property || reservation.propertyInformation; +function DetailsModal({ r, isOpen, onClose }) { + if (!isOpen || !r) return null; + const p = r._prop; return ( - - e.stopPropagation()} - > + + e.stopPropagation()}>

تفاصيل الحجز

- +
-

رقم الحجز: #{reservation.id}

+

رقم الحجز: #{r.id}

-
- {property && ( -
-

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

-

العنوان: {property.address || '—'}

- {property.numberOfBedRooms && ( -
- {property.numberOfBedRooms} غرف - {property.numberOfBathRooms} حمامات -
- )} -
- )} - + {p &&
+

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

+

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

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

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

+

تفاصيل الحجز

-
-

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

-

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

-
-
-

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

-

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

-
-
-

الحالة

-

-
-
-

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

-

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

-
+

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

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

+

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

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

+

الحالة

+

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

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

-
-

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

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

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

+
الإجمالي{r.totalPrice?.toLocaleString()??'—'}
@@ -208,129 +149,75 @@ export default function UserReservationsPage() { const [filterStatus, setFilterStatus] = useState('all'); const [searchTerm, setSearchTerm] = useState(''); - useEffect(() => { - const user = AuthService.getUser(); - if (!user) { - router.push('/login'); - return; - } - loadReservations(); - }, [router]); + useEffect(() => { if (!AuthService.getUser()) { router.push('/login'); return; } loadReservations(); }, [router]); - const loadReservations = async () => { + const loadReservations = useCallback(async () => { try { - const token = AuthService.getToken(); const res = await fetch(`${API_BASE}/Reservations/GetUserResevations`, { - headers: { Authorization: `Bearer ${token}` }, + headers: { Authorization: `Bearer ${AuthService.getToken()}` }, }); - if (!res.ok) throw new Error(`HTTP ${res.status}`); const json = await res.json(); - let list = json.data || json || []; if (!Array.isArray(list)) list = []; - - // Enrich each reservation with property details - const enriched = await Promise.all( - list.map(async (r) => { - if (!r.property && r.propertyId) { - try { - const propRes = await getRentProperty(r.propertyId); - r.property = propRes?.data?.propertyInformation || propRes?.data || propRes || null; - } catch (e) { - console.warn('Failed to load property', r.propertyId); - } - } - return r; - }) - ); - + const enriched = await Promise.all(list.map(enrich)); setReservations(enriched); setFiltered(enriched); } catch (err) { - console.error('Failed to load reservations:', err); + console.error(err); toast.error('فشل تحميل الحجوزات'); setReservations([]); setFiltered([]); } setLoading(false); - }; + }, []); useEffect(() => { - let result = reservations; - if (filterStatus !== 'all') result = result.filter(r => r.status === filterStatus); - if (searchTerm) { - const q = searchTerm.toLowerCase(); - result = result.filter(r => { - const addr = (r.property?.address || '').toLowerCase(); - const sid = String(r.id).toLowerCase(); - return addr.includes(q) || sid.includes(q); - }); - } - setFiltered(result); + 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).toLowerCase().includes(q) || String(x.id).includes(q)); } + setFiltered(r); }, [reservations, filterStatus, searchTerm]); - const statuses = ['all', ...new Set(reservations.map(r => r.status))]; - const statusCounts = { - all: reservations.length, - ...Object.fromEntries( - [...new Set(reservations.map(r => r.status))].map(s => [s, reservations.filter(r => r.status === s).length]) - ), - }; + 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
; + if (loading) return
; return (
- setSelected(null)} /> - + setSelected(null)} />
- - + +

حجوزاتي

لديك {reservations.length} حجز

-
- {Object.entries(statusCounts).map(([status, count]) => ( - setFilterStatus(status)} - > -
{count}
-
{status === 'all' ? 'الكل' : (statusConfig[status]?.label || status)}
+ {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" - /> + + 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 => )}
)}