Added translation to all site with edit style
All checks were successful
Build frontend / build (push) Successful in 1m40s
All checks were successful
Build frontend / build (push) Successful in 1m40s
This commit is contained in:
@ -643,12 +643,12 @@
|
||||
|
||||
|
||||
|
||||
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
Calendar,
|
||||
Clock,
|
||||
@ -660,8 +660,6 @@ import {
|
||||
MapPin,
|
||||
DollarSign,
|
||||
Home,
|
||||
ArrowLeft,
|
||||
RefreshCw,
|
||||
Flag,
|
||||
} from 'lucide-react';
|
||||
import toast, { Toaster } from 'react-hot-toast';
|
||||
@ -680,34 +678,37 @@ const STATUS_MAP = [
|
||||
'cancelled',
|
||||
];
|
||||
|
||||
const STATUS_I18N_KEYS = {
|
||||
pending: 'bookingStatus.pending',
|
||||
ownerConfirmed: 'bookingStatus.ownerConfirmed',
|
||||
depositPaid: 'bookingStatus.depositPaid',
|
||||
depositConfirmed: 'bookingStatus.depositConfirmed',
|
||||
completed: 'bookingStatus.completed',
|
||||
cancelled: 'bookingStatus.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,
|
||||
},
|
||||
@ -715,7 +716,7 @@ const STATUS_UI = {
|
||||
|
||||
const getStatusKey = (code) => STATUS_MAP[Number(code)] || 'pending';
|
||||
|
||||
const sLabel = (code) => STATUS_UI[getStatusKey(code)]?.label ?? String(code);
|
||||
const sLabel = (code, t) => t(STATUS_I18N_KEYS[getStatusKey(code)]) || String(code);
|
||||
|
||||
const sColor = (code) =>
|
||||
STATUS_UI[getStatusKey(code)]?.color ?? 'bg-gray-100 text-gray-700';
|
||||
@ -723,6 +724,7 @@ const sColor = (code) =>
|
||||
const sIcon = (code) => STATUS_UI[getStatusKey(code)]?.icon ?? Clock;
|
||||
|
||||
function StatusBadge({ code }) {
|
||||
const { t } = useTranslation();
|
||||
const Icon = sIcon(code);
|
||||
|
||||
return (
|
||||
@ -731,7 +733,7 @@ function StatusBadge({ code }) {
|
||||
code
|
||||
)}`}
|
||||
>
|
||||
<Icon className="w-3 h-3" /> {sLabel(code)}
|
||||
<Icon className="w-3 h-3" /> {sLabel(code, t)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@ -839,10 +841,10 @@ async function reportReservation(reservationId, message) {
|
||||
const rid = Number(reservationId);
|
||||
|
||||
if (!Number.isInteger(rid)) {
|
||||
throw new Error('رقم الحجز غير صالح');
|
||||
throw new Error(t('invalidReservationId'));
|
||||
}
|
||||
if (!Number.isInteger(reporter)) {
|
||||
throw new Error('تعذر تحديد المستخدم الحالي');
|
||||
throw new Error(t('couldNotIdentifyUser'));
|
||||
}
|
||||
|
||||
const token = getAuthToken();
|
||||
@ -860,7 +862,7 @@ async function reportReservation(reservationId, message) {
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
let errorMessage = 'فشل إرسال البلاغ';
|
||||
let errorMessage = t('reportFailed');
|
||||
try {
|
||||
const data = await res.json();
|
||||
errorMessage = data?.message || data?.title || errorMessage;
|
||||
@ -889,6 +891,7 @@ function OwnerCard({
|
||||
actionLoadingId,
|
||||
reportingId,
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const p = r._prop;
|
||||
const imgs = pImgs(p);
|
||||
const img = imgs.length > 0 ? buildImageUrl(imgs[0]) : null;
|
||||
@ -926,21 +929,21 @@ function OwnerCard({
|
||||
<div className="text-lg font-bold text-amber-600">
|
||||
{r.totalPrice?.toLocaleString() ?? '—'}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500">السعر الإجمالي</div>
|
||||
<div className="text-xs text-gray-500">{t('totalPrice')}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{(pBeds(p) || pBaths(p)) && (
|
||||
<div className="flex gap-3 mb-3 text-sm text-gray-600">
|
||||
{pBeds(p) > 0 && <span>{pBeds(p)} غرف</span>}
|
||||
{pBaths(p) > 0 && <span>{pBaths(p)} حمامات</span>}
|
||||
{pBeds(p) > 0 && <span>{pBeds(p)} {t('reservationsBedrooms')}</span>}
|
||||
{pBaths(p) > 0 && <span>{pBaths(p)} {t('reservationsBathrooms')}</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-xs text-gray-500">{t('from')}</div>
|
||||
<div className="text-sm font-medium">
|
||||
{r.startDate
|
||||
? new Date(r.startDate).toLocaleDateString('ar')
|
||||
@ -950,7 +953,7 @@ function OwnerCard({
|
||||
|
||||
<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-xs text-gray-500">{t('to')}</div>
|
||||
<div className="text-sm font-medium">
|
||||
{r.endDate ? new Date(r.endDate).toLocaleDateString('ar') : '—'}
|
||||
</div>
|
||||
@ -968,7 +971,7 @@ function OwnerCard({
|
||||
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" />
|
||||
التفاصيل
|
||||
{t('details')}
|
||||
</button>
|
||||
|
||||
{isPending && (
|
||||
@ -984,7 +987,7 @@ function OwnerCard({
|
||||
) : (
|
||||
<CheckCircle className="w-4 h-4" />
|
||||
)}
|
||||
قبول
|
||||
{t('accept')}
|
||||
</button>
|
||||
|
||||
<button
|
||||
@ -998,7 +1001,7 @@ function OwnerCard({
|
||||
) : (
|
||||
<XCircle className="w-4 h-4" />
|
||||
)}
|
||||
رفض
|
||||
{t('reject')}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
@ -1019,7 +1022,7 @@ function OwnerCard({
|
||||
) : (
|
||||
<Flag className="w-4 h-4" />
|
||||
)}
|
||||
{isReporting ? 'جاري الإبلاغ...' : 'إبلاغ'}
|
||||
{isReporting ? t('reporting') : t('report')}
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
@ -1027,6 +1030,7 @@ function OwnerCard({
|
||||
}
|
||||
|
||||
function DetailsModal({ r, isOpen, onClose, onReport, reportingId }) {
|
||||
const { t } = useTranslation();
|
||||
if (!isOpen || !r) return null;
|
||||
|
||||
const p = r._prop;
|
||||
@ -1049,7 +1053,7 @@ function DetailsModal({ r, isOpen, onClose, onReport, reportingId }) {
|
||||
>
|
||||
<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">طلب حجز #{r.id}</h2>
|
||||
<h2 className="text-xl font-bold">{t('reservationRequest', { id: r.id })}</h2>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
@ -1066,11 +1070,11 @@ function DetailsModal({ r, isOpen, onClose, onReport, reportingId }) {
|
||||
<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" />
|
||||
معلومات العقار
|
||||
{t('propertyInfo')}
|
||||
</h3>
|
||||
|
||||
<p>
|
||||
<span className="text-gray-500">العنوان:</span>{' '}
|
||||
<span className="text-gray-500">{t('address')}</span>{' '}
|
||||
{pAddr(p) || '—'}
|
||||
</p>
|
||||
|
||||
@ -1078,13 +1082,13 @@ function DetailsModal({ r, isOpen, onClose, onReport, reportingId }) {
|
||||
<div className="flex gap-3 mt-2">
|
||||
{pBeds(p) > 0 && (
|
||||
<span className="text-sm bg-white px-2 py-1 rounded-lg">
|
||||
{pBeds(p)} غرف
|
||||
{pBeds(p)} {t('reservationsBedrooms')}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{pBaths(p) > 0 && (
|
||||
<span className="text-sm bg-white px-2 py-1 rounded-lg">
|
||||
{pBaths(p)} حمامات
|
||||
{pBaths(p)} {t('reservationsBathrooms')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@ -1095,12 +1099,12 @@ function DetailsModal({ r, isOpen, onClose, onReport, reportingId }) {
|
||||
<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" />
|
||||
تفاصيل الحجز
|
||||
{t('reservationDetails')}
|
||||
</h3>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<p className="text-gray-500">تاريخ البداية</p>
|
||||
<p className="text-gray-500">{t('startDate')}</p>
|
||||
<p className="font-medium">
|
||||
{r.startDate
|
||||
? new Date(r.startDate).toLocaleDateString('ar')
|
||||
@ -1109,19 +1113,19 @@ function DetailsModal({ r, isOpen, onClose, onReport, reportingId }) {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="text-gray-500">تاريخ النهاية</p>
|
||||
<p className="text-gray-500">{t('endDate')}</p>
|
||||
<p className="font-medium">
|
||||
{r.endDate ? new Date(r.endDate).toLocaleDateString('ar') : '—'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="text-gray-500">الحالة</p>
|
||||
<p className="text-gray-500">{t('statusLabel')}</p>
|
||||
<StatusBadge code={r.status} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="text-gray-500">تاريخ الإنشاء</p>
|
||||
<p className="text-gray-500">{t('createdAt')}</p>
|
||||
<p className="font-medium">
|
||||
{r.createdAt
|
||||
? new Date(r.createdAt).toLocaleDateString('ar')
|
||||
@ -1134,11 +1138,11 @@ function DetailsModal({ r, isOpen, onClose, onReport, reportingId }) {
|
||||
<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" />
|
||||
المعلومات المالية
|
||||
{t('financialInfo')}
|
||||
</h3>
|
||||
|
||||
<div className="flex justify-between font-bold">
|
||||
<span className="text-gray-900">الإجمالي</span>
|
||||
<span className="text-gray-900">{t('total')}</span>
|
||||
<span className="text-amber-600 text-lg">
|
||||
{r.totalPrice?.toLocaleString() ?? '—'}
|
||||
</span>
|
||||
@ -1151,6 +1155,7 @@ function DetailsModal({ r, isOpen, onClose, onReport, reportingId }) {
|
||||
}
|
||||
|
||||
function ReportDialog({ isOpen, reservation, onClose, onSubmit, submitting }) {
|
||||
const { t } = useTranslation();
|
||||
const [message, setMessage] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
@ -1177,8 +1182,8 @@ function ReportDialog({ isOpen, reservation, onClose, onSubmit, submitting }) {
|
||||
<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>
|
||||
<h2 className="text-xl font-bold">{t('reportReservation')}</h2>
|
||||
<p className="text-red-100 text-sm mt-1">{t('reservationId', { id: reservation.id })}</p>
|
||||
</div>
|
||||
<button onClick={onClose} className="rounded-full p-1 hover:bg-white/20">
|
||||
<XCircle className="h-6 w-6" />
|
||||
@ -1188,13 +1193,13 @@ function ReportDialog({ isOpen, reservation, onClose, onSubmit, submitting }) {
|
||||
|
||||
<div className="p-6">
|
||||
<p className="text-gray-700 mb-4 leading-7">
|
||||
اخبر فريق الدعم بما حدث التفاصيل الواضحة تساعدنا على مراجعة هذا الحجز بشكل اسرع
|
||||
{t('reportDescription')}
|
||||
</p>
|
||||
|
||||
<textarea
|
||||
value={message}
|
||||
onChange={(e) => setMessage(e.target.value)}
|
||||
placeholder="اكتب تفاصيل البلاغ هنا..."
|
||||
placeholder={t('reportPlaceholder')}
|
||||
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"
|
||||
/>
|
||||
@ -1208,14 +1213,14 @@ function ReportDialog({ isOpen, reservation, onClose, onSubmit, submitting }) {
|
||||
}`}
|
||||
>
|
||||
{submitting ? <Loader2 className="h-4 w-4 animate-spin" /> : <Flag className="h-4 w-4" />}
|
||||
{submitting ? 'جاري الإرسال...' : 'إرسال البلاغ'}
|
||||
{submitting ? t('submitting') : t('submitReport')}
|
||||
</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"
|
||||
>
|
||||
إلغاء
|
||||
{t('cancel')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -1225,6 +1230,7 @@ function ReportDialog({ isOpen, reservation, onClose, onSubmit, submitting }) {
|
||||
}
|
||||
|
||||
export default function OwnerReservationRequestsPage() {
|
||||
const { t, i18n } = useTranslation();
|
||||
const router = useRouter();
|
||||
|
||||
const [reservations, setReservations] = useState([]);
|
||||
@ -1270,7 +1276,7 @@ export default function OwnerReservationRequestsPage() {
|
||||
setFiltered(enriched);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
toast.error('فشل تحميل طلبات الحجز');
|
||||
toast.error(t('loadFailedReservations'));
|
||||
setReservations([]);
|
||||
setFiltered([]);
|
||||
} finally {
|
||||
@ -1321,18 +1327,18 @@ export default function OwnerReservationRequestsPage() {
|
||||
throw new Error(errorText || `HTTP ${res.status}`);
|
||||
}
|
||||
|
||||
toast.success('تم قبول الحجز بنجاح');
|
||||
toast.success(t('acceptSuccess'));
|
||||
await loadReservations();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
toast.error('فشل قبول الحجز');
|
||||
toast.error(t('acceptFailed'));
|
||||
} finally {
|
||||
setActionLoadingId(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleReject = async (r) => {
|
||||
if (!confirm('هل أنت متأكد من رفض هذا الحجز؟')) return;
|
||||
if (!confirm(t('rejectConfirm'))) return;
|
||||
|
||||
try {
|
||||
setActionLoadingId(r.id);
|
||||
@ -1348,11 +1354,11 @@ export default function OwnerReservationRequestsPage() {
|
||||
throw new Error(errorText || `HTTP ${res.status}`);
|
||||
}
|
||||
|
||||
toast.success('تم رفض الحجز');
|
||||
toast.success(t('rejectSuccess'));
|
||||
await loadReservations();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
toast.error('فشل رفض الحجز');
|
||||
toast.error(t('rejectFailed'));
|
||||
} finally {
|
||||
setActionLoadingId(null);
|
||||
}
|
||||
@ -1372,11 +1378,11 @@ export default function OwnerReservationRequestsPage() {
|
||||
setReportingId(reportDialog.reservation.id);
|
||||
try {
|
||||
await reportReservation(reportDialog.reservation.id, message.trim() || null);
|
||||
toast.success('تم إرسال البلاغ بنجاح');
|
||||
toast.success(t('reportSuccess'));
|
||||
closeReportDialog();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
toast.error(err?.message || 'فشل إرسال البلاغ');
|
||||
toast.error(err?.message || t('reportFailed'));
|
||||
} finally {
|
||||
setReportingId(null);
|
||||
}
|
||||
@ -1405,7 +1411,7 @@ export default function OwnerReservationRequestsPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 py-8" dir="rtl">
|
||||
<div className="min-h-screen bg-gray-50 py-8" dir={i18n.language === 'ar' ? 'rtl' : 'ltr'}>
|
||||
<Toaster position="top-center" reverseOrder={false} />
|
||||
|
||||
<DetailsModal
|
||||
@ -1429,30 +1435,11 @@ export default function OwnerReservationRequestsPage() {
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="mb-8"
|
||||
>
|
||||
<button
|
||||
type="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>
|
||||
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900">
|
||||
طلبات الحجز
|
||||
</h1>
|
||||
<p className="text-gray-600">لديك {reservations.length} طلب</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={loadReservations}
|
||||
className="p-2 bg-white shadow rounded-xl hover:shadow-md transition-all"
|
||||
>
|
||||
<RefreshCw className="w-5 h-5 text-gray-600" />
|
||||
</button>
|
||||
<div className="mb-4">
|
||||
<h1 className="text-3xl font-bold text-gray-900">
|
||||
{t('reservationsTitle')}
|
||||
</h1>
|
||||
<p className="text-gray-600">{t('reservationsCount', { count: reservations.length })}</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
@ -1471,7 +1458,7 @@ export default function OwnerReservationRequestsPage() {
|
||||
>
|
||||
<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}
|
||||
{s === 'all' ? t('filterAll') : t(STATUS_I18N_KEYS[s]) || s}
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
@ -1482,7 +1469,7 @@ export default function OwnerReservationRequestsPage() {
|
||||
|
||||
<input
|
||||
type="text"
|
||||
placeholder="ابحث بعنوان العقار أو رقم الحجز..."
|
||||
placeholder={t('searchPlaceholderReservations')}
|
||||
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"
|
||||
@ -1493,10 +1480,10 @@ export default function OwnerReservationRequestsPage() {
|
||||
<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">
|
||||
لا توجد طلبات
|
||||
{t('noReservations')}
|
||||
</h3>
|
||||
<p className="text-gray-600">
|
||||
لم يتم استلام أي طلبات حجز حتى الآن
|
||||
{t('noReservationsHint')}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
@ -1518,4 +1505,4 @@ export default function OwnerReservationRequestsPage() {
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user