forked from amer2004/SweetHome
Added translation to all site with edit style
This commit is contained in:
@ -396,6 +396,7 @@ import { useState, useEffect, useCallback } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { motion } from 'framer-motion';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
Calendar,
|
||||
Clock,
|
||||
@ -432,50 +433,49 @@ const API_BASE = process.env.NEXT_PUBLIC_API_URL || 'https://45.93.137.91.nip.io
|
||||
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 },
|
||||
pending: { color: 'bg-yellow-100 text-yellow-800', icon: Clock },
|
||||
ownerConfirmed: { color: 'bg-blue-100 text-blue-800', icon: CheckCircle },
|
||||
depositPaid: { color: 'bg-indigo-100 text-indigo-800', icon: DollarSign },
|
||||
depositConfirmed: { color: 'bg-green-100 text-green-800', icon: CheckCircle },
|
||||
completed: { color: 'bg-green-100 text-green-800', icon: CheckCircle },
|
||||
cancelled: { color: 'bg-gray-100 text-gray-800', icon: XCircle },
|
||||
};
|
||||
|
||||
const PAYMENT_METHODS = [
|
||||
{
|
||||
id: 'cash',
|
||||
label: 'الدفع النقدي',
|
||||
desc: 'ادفع الآن عبر شام كاش مع تأكيد تلقائي للحجز',
|
||||
labelKey: 'payment.cash',
|
||||
descKey: 'payment.cashDesc',
|
||||
icon: Banknote,
|
||||
active: true,
|
||||
},
|
||||
{
|
||||
id: 'office',
|
||||
label: 'نقداً',
|
||||
desc: 'ادفع في مكتب المنصة. يتم التأكيد لاحقاً',
|
||||
labelKey: 'payment.office',
|
||||
descKey: 'payment.officeDesc',
|
||||
icon: Building,
|
||||
active: true,
|
||||
},
|
||||
{
|
||||
id: 'transfer',
|
||||
label: 'تحويل داخلي',
|
||||
desc: 'حول من حساب داخلي ثم أرسل المرجع',
|
||||
labelKey: 'payment.transfer',
|
||||
descKey: 'payment.transferDesc',
|
||||
icon: ArrowLeftRight,
|
||||
active: false,
|
||||
},
|
||||
{
|
||||
id: 'electronic',
|
||||
label: 'دفع إلكتروني',
|
||||
desc: 'سيتم تفعيل الدفع الإلكتروني والتحويل لاحقاً',
|
||||
labelKey: 'payment.electronic',
|
||||
descKey: 'payment.electronicDesc',
|
||||
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 = 'ل.س') {
|
||||
function formatCurrency(v, sign = '') {
|
||||
return `${sign} ${Number(v ?? 0).toLocaleString()}`;
|
||||
}
|
||||
|
||||
@ -487,10 +487,12 @@ function formatDate(date) {
|
||||
}
|
||||
|
||||
function StatusBadge({ code }) {
|
||||
const { t } = useTranslation();
|
||||
const Icon = statusIcon(code);
|
||||
const key = STATUS_MAP[code] || 'pending';
|
||||
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)}
|
||||
<Icon className="w-3 h-3" /> {t(`bookingStatus.${key}`)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@ -563,10 +565,10 @@ async function reportReservation(reservationId, message) {
|
||||
const rid = Number(reservationId);
|
||||
|
||||
if (!Number.isInteger(rid)) {
|
||||
throw new Error('رقم الحجز غير صالح');
|
||||
throw new Error('Invalid reservation ID');
|
||||
}
|
||||
if (!Number.isInteger(reporter)) {
|
||||
throw new Error('تعذر تحديد المستخدم الحالي');
|
||||
throw new Error('Could not identify current user');
|
||||
}
|
||||
|
||||
const token = getAuthToken();
|
||||
@ -584,7 +586,7 @@ async function reportReservation(reservationId, message) {
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
let errorMessage = 'فشل إرسال البلاغ';
|
||||
let errorMessage = 'Failed to submit report';
|
||||
try {
|
||||
const data = await res.json();
|
||||
errorMessage = data?.message || data?.title || errorMessage;
|
||||
@ -640,6 +642,7 @@ function formatWindowDuration(str) {
|
||||
}
|
||||
|
||||
function CountdownTimer({ deadline }) {
|
||||
const { t } = useTranslation();
|
||||
const [remaining, setRemaining] = useState(deadline ? Math.max(0, deadline - Date.now()) : 0);
|
||||
useEffect(() => {
|
||||
if (!deadline) return;
|
||||
@ -648,7 +651,7 @@ function CountdownTimer({ deadline }) {
|
||||
const id = setInterval(tick, 1000);
|
||||
return () => clearInterval(id);
|
||||
}, [deadline]);
|
||||
if (remaining <= 0) return <span className="text-red-500 text-sm font-medium">انتهت المهلة</span>;
|
||||
if (remaining <= 0) return <span className="text-red-500 text-sm font-medium">{t('timeExpired')}</span>;
|
||||
const h = Math.floor(remaining / 3600000);
|
||||
const m = Math.floor((remaining % 3600000) / 60000);
|
||||
const s = Math.floor((remaining % 60000) / 1000);
|
||||
@ -665,6 +668,7 @@ function PaymentDialog({
|
||||
onConfirmPay,
|
||||
payingId,
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [showCashDialog, setShowCashDialog] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
@ -674,7 +678,7 @@ function PaymentDialog({
|
||||
if (!isOpen || !reservation) return null;
|
||||
|
||||
const amount = reservation.depositAmount || reservation.totalPrice || 0;
|
||||
const currencySign = reservation.currencySign || 'ل.س';
|
||||
const currencySign = reservation.currencySign || t('sypSymbol');
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -720,12 +724,12 @@ function PaymentDialog({
|
||||
|
||||
<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-sm text-gray-500 mb-1">{t('depositAmount')}</p>
|
||||
<p className="text-4xl font-bold text-amber-600">
|
||||
{formatCurrency(amount, currencySign)}
|
||||
</p>
|
||||
<p className="text-xs text-gray-400 mt-2">
|
||||
يتم دفع التأمين إلى المنصة وليس إلى المالك
|
||||
{t('depositPaidToPlatform')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -734,13 +738,13 @@ function PaymentDialog({
|
||||
<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">
|
||||
الدفع النقدي فقط هو المتاح. سيتم تفعيل الدفع الإلكتروني والتحويل لاحقاً.
|
||||
{t('cashOnlyNotice')}
|
||||
</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>
|
||||
<p className="text-sm font-bold text-gray-700 mb-3">{t('paymentMethod')}</p>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
{PAYMENT_METHODS.map((method) => {
|
||||
const isSelected = selectedPayment === method.id;
|
||||
@ -767,14 +771,14 @@ function PaymentDialog({
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className={`text-sm font-bold ${isSelected ? 'text-amber-900' : 'text-gray-800'}`}>
|
||||
{method.label}
|
||||
{t(method.labelKey)}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 mt-0.5 leading-relaxed">
|
||||
{method.desc}
|
||||
{t(method.descKey)}
|
||||
</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">
|
||||
غير متاح
|
||||
{t('notAvailable')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@ -801,12 +805,12 @@ function PaymentDialog({
|
||||
{payingId === reservation.id ? (
|
||||
<>
|
||||
<Loader2 className="w-5 h-5 animate-spin" />
|
||||
جاري الدفع...
|
||||
{t('processingPayment')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Banknote className="w-5 h-5" />
|
||||
دفع التأمين ({formatCurrency(amount, currencySign)})
|
||||
{t('payDeposit', { amount: formatCurrency(amount, currencySign) })}
|
||||
</>
|
||||
)}
|
||||
</motion.button>
|
||||
@ -819,7 +823,7 @@ function PaymentDialog({
|
||||
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" />
|
||||
سأدفع نقداً
|
||||
{t('payCash')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@ -827,9 +831,9 @@ function PaymentDialog({
|
||||
<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 font-bold text-gray-800 mb-1">{t('cashPaymentLocation')}</p>
|
||||
<p className="text-sm text-gray-600 leading-relaxed">
|
||||
مكتب المنصة: أبو رمانة، شارع المالكي، دمشق
|
||||
{t('platformOfficeAddress')}
|
||||
</p>
|
||||
<p className="text-sm text-gray-600 flex items-center gap-1 mt-1">
|
||||
<Phone className="w-3.5 h-3.5" />
|
||||
@ -845,7 +849,7 @@ function PaymentDialog({
|
||||
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"
|
||||
>
|
||||
إغلاق
|
||||
{t('close')}
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
@ -861,15 +865,13 @@ function PaymentDialog({
|
||||
<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>
|
||||
<h3 className="text-xl font-bold text-gray-900 mb-3">{t('cashPaymentPending')}</h3>
|
||||
<p className="text-gray-600 leading-relaxed mb-6">
|
||||
اذهب إلى موقع المنصة وادفع العربون.
|
||||
<br />
|
||||
ستقوم المنصة بتأكيد الدفع بعد استلام المبلغ.
|
||||
{t('cashPaymentInstructions')}
|
||||
</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 font-bold text-gray-800 mb-1">{t('platformLocation')}</p>
|
||||
<p className="text-sm text-gray-600">{t('platformOfficeAddress')}</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
|
||||
@ -881,13 +883,13 @@ function PaymentDialog({
|
||||
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"
|
||||
>
|
||||
إغلاق
|
||||
{t('close')}
|
||||
</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"
|
||||
>
|
||||
حجوزاتي
|
||||
{t('myBookings')}
|
||||
</Link>
|
||||
</div>
|
||||
</motion.div>
|
||||
@ -898,6 +900,7 @@ function PaymentDialog({
|
||||
}
|
||||
|
||||
function ReportDialog({ isOpen, reservation, onClose, onSubmit, submitting }) {
|
||||
const { t } = useTranslation();
|
||||
const [message, setMessage] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
@ -924,8 +927,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('reservationNumber', { id: reservation.id })}</p>
|
||||
</div>
|
||||
<button onClick={onClose} className="rounded-full p-1 hover:bg-white/20">
|
||||
<XCircle className="h-6 w-6" />
|
||||
@ -935,13 +938,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"
|
||||
/>
|
||||
@ -955,14 +958,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('sending') : t('sendReport')}
|
||||
</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>
|
||||
@ -972,6 +975,7 @@ function ReportDialog({ isOpen, reservation, onClose, onSubmit, submitting }) {
|
||||
}
|
||||
|
||||
function ReservationCard({ r, onViewDetails, onPay, onReport, payingId, reportingId }) {
|
||||
const { t } = useTranslation();
|
||||
const p = r._prop;
|
||||
const imgs = propImages(p, r);
|
||||
const img = imgs.length > 0 ? `${API_BASE}${imgs[0]}` : null;
|
||||
@ -1004,54 +1008,54 @@ function ReservationCard({ r, onViewDetails, onPay, onReport, payingId, reportin
|
||||
</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 className="text-xs text-gray-500">{t('totalPrice')}</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>}
|
||||
{(beds||baths) && <div className="flex gap-3 mb-3 text-sm text-gray-600">{beds>0&&<span>{beds} {t('rooms')}</span>}{baths>0&&<span>{baths} {t('bathrooms')}</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>
|
||||
<Calendar className="w-4 h-4 text-amber-500 mx-auto mb-1"/><div className="text-xs text-gray-500">{t('from')}</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>
|
||||
<Calendar className="w-4 h-4 text-amber-500 mx-auto mb-1"/><div className="text-xs text-gray-500">{t('to')}</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>
|
||||
<span className="text-sm text-blue-800 font-medium flex items-center gap-1"><Timer className="w-4 h-4"/> {t('remainingForPayment')}</span>
|
||||
<CountdownTimer deadline={deadline} />
|
||||
</div>
|
||||
<div className="text-xs text-blue-600">مدة الدفع: {formatWindowDuration(p.allowedPaymentPeriod)}</div>
|
||||
<div className="text-xs text-blue-600">{t('paymentDuration', { duration: 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"/> التفاصيل
|
||||
<Eye className="w-4 h-4"/> {t('details')}
|
||||
</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 ? 'جاري الدفع...' : 'ادفع الآن'}
|
||||
{isPaying ? <Loader2 className="w-4 h-4 animate-spin"/> : <CreditCard className="w-4 h-4"/>} {isPaying ? t('processingPayment') : t('payNow')}
|
||||
</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 ? 'جاري الإبلاغ...' : 'إبلاغ'}
|
||||
{isReporting ? <Loader2 className="w-4 h-4 animate-spin"/> : <Flag className="w-4 h-4"/>} {isReporting ? t('reportingAction') : t('report')}
|
||||
</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"/> قيّم هذا العقار
|
||||
<Star className="w-4 h-4"/> {t('rateThisProperty')}
|
||||
</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: 'التجربة العامة' },
|
||||
{ key: 'clean', label: 'ratingCategory.clean' },
|
||||
{ key: 'services', label: 'ratingCategory.services' },
|
||||
{ key: 'ownerBehavior', label: 'ratingCategory.ownerBehavior' },
|
||||
{ key: 'experience', label: 'ratingCategory.experience' },
|
||||
].map(cat => <div key={cat.key} className="flex items-center justify-between">
|
||||
<span className="text-sm text-gray-700">{cat.label}</span>
|
||||
<span className="text-sm text-gray-700">{t(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}))}
|
||||
@ -1063,26 +1067,26 @@ function ReservationCard({ r, onViewDetails, onPay, onReport, payingId, reportin
|
||||
</div>)}
|
||||
</div>
|
||||
<textarea value={ratingComment} onChange={e => setRatingComment(e.target.value)}
|
||||
placeholder="أكتب تعليقك (اختياري)"
|
||||
placeholder={t('writeCommentOptional')}
|
||||
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('قيّم جميع الفئات');
|
||||
if (!ratings.clean || !ratings.services || !ratings.ownerBehavior || !ratings.experience) return toast.error(t('rateAllCategories'));
|
||||
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('تم إرسال التقييم');
|
||||
toast.success(t('ratingSubmittedToast'));
|
||||
setShowRating(false);
|
||||
setRatings({ clean: 0, services: 0, ownerBehavior: 0, experience: 0 });
|
||||
setRatingComment('');
|
||||
} catch (e) { toast.error(e?.message || 'فشل إرسال التقييم'); }
|
||||
} catch (e) { toast.error(e?.message || t('ratingFailed')); }
|
||||
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 ? 'جاري الإرسال...' : 'إرسال التقييم'}
|
||||
{submittingRating ? t('sending') : t('submitRating')}
|
||||
</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>
|
||||
className="px-4 py-1.5 bg-gray-200 text-gray-700 rounded-lg text-sm hover:bg-gray-300 transition-colors">{t('cancel')}</button>
|
||||
</div>
|
||||
</div>}
|
||||
</div>
|
||||
@ -1091,6 +1095,7 @@ function ReservationCard({ r, onViewDetails, onPay, onReport, payingId, reportin
|
||||
}
|
||||
|
||||
function DetailsModal({ r, isOpen, onClose, onPay, onReport, payingId, reportingId }) {
|
||||
const { t } = useTranslation();
|
||||
if (!isOpen || !r) return null;
|
||||
const p = r._prop;
|
||||
const isOwnerConfirmed = STATUS_MAP[r.status] === 'ownerConfirmed';
|
||||
@ -1109,47 +1114,47 @@ function DetailsModal({ r, isOpen, onClose, onPay, onReport, payingId, reporting
|
||||
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>
|
||||
<h2 className="text-xl font-bold">{t('bookingDetails')}</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>
|
||||
<p className="text-amber-100 text-sm mt-1">{t('reservationNumber', { id: 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>
|
||||
<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">{t('addressLabel')}</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>}
|
||||
{propBeds(p, r)>0&&<span className="text-sm bg-white px-2 py-1 rounded-lg">{propBeds(p, r)} {t('rooms')}</span>}
|
||||
{propBaths(p, r)>0&&<span className="text-sm bg-white px-2 py-1 rounded-lg">{propBaths(p, r)} {t('bathrooms')}</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>
|
||||
<h3 className="font-bold text-gray-900 mb-3 flex items-center gap-2"><Calendar className="w-5 h-5 text-amber-500"/> {t('bookingDetails')}</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><p className="text-gray-500">{t('startDateLabel')}</p><p className="font-medium">{new Date(r.startDate).toLocaleDateString('ar')}</p></div>
|
||||
<div><p className="text-gray-500">{t('endDateLabel')}</p><p className="font-medium">{new Date(r.endDate).toLocaleDateString('ar')}</p></div>
|
||||
<div><p className="text-gray-500">{t('statusLabel')}</p><StatusBadge code={r.status}/></div>
|
||||
<div><p className="text-gray-500">{t('createdAtLabel')}</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>
|
||||
<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">{t('total')}</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>
|
||||
<span className="text-blue-800 font-medium flex items-center gap-2"><Timer className="w-5 h-5"/> {t('remainingForPayment')}</span>
|
||||
<CountdownTimer deadline={deadline} />
|
||||
</div>
|
||||
<div className="text-xs text-blue-600 mb-3">مدة الدفع: {formatWindowDuration(p.allowedPaymentPeriod)}</div>
|
||||
<div className="text-xs text-blue-600 mb-3">{t('paymentDuration', { duration: 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 ? 'جاري الدفع...' : 'ادفع الآن'}
|
||||
{isPaying ? <Loader2 className="w-5 h-5 animate-spin"/> : <CreditCard className="w-5 h-5"/>} {isPaying ? t('processingPayment') : t('payNow')}
|
||||
</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 ? 'جاري الإبلاغ...' : 'إبلاغ'}
|
||||
{isReporting ? <Loader2 className="w-5 h-5 animate-spin"/> : <Flag className="w-5 h-5"/>} {isReporting ? t('reportingAction') : t('report')}
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
@ -1158,6 +1163,7 @@ function DetailsModal({ r, isOpen, onClose, onPay, onReport, payingId, reporting
|
||||
}
|
||||
|
||||
export default function UserReservationsPage() {
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
const [reservations, setReservations] = useState([]);
|
||||
const [filtered, setFiltered] = useState([]);
|
||||
@ -1196,7 +1202,7 @@ export default function UserReservationsPage() {
|
||||
setFiltered(enriched);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
toast.error('فشل تحميل الحجوزات');
|
||||
toast.error(t('failedToLoadBookings'));
|
||||
setReservations([]);
|
||||
setFiltered([]);
|
||||
}
|
||||
@ -1233,11 +1239,11 @@ export default function UserReservationsPage() {
|
||||
transactionType: 1,
|
||||
comment: null,
|
||||
});
|
||||
toast.success('تم دفع السلفة بنجاح!');
|
||||
toast.success(t('depositPaidSuccess'));
|
||||
closePaymentDialog();
|
||||
loadReservations();
|
||||
} catch (err) {
|
||||
toast.error(err?.message || 'فشل عملية الدفع');
|
||||
toast.error(err?.message || t('paymentFailed'));
|
||||
} finally {
|
||||
setPayingId(null);
|
||||
}
|
||||
@ -1257,10 +1263,10 @@ export default function UserReservationsPage() {
|
||||
setReportingId(reportDialog.reservation.id);
|
||||
try {
|
||||
await reportReservation(reportDialog.reservation.id, message.trim() || null);
|
||||
toast.success('تم إرسال البلاغ بنجاح');
|
||||
toast.success(t('reportSubmittedSuccess'));
|
||||
closeReportDialog();
|
||||
} catch (err) {
|
||||
toast.error(err?.message || 'فشل إرسال البلاغ');
|
||||
toast.error(err?.message || t('reportFailed'));
|
||||
} finally {
|
||||
setReportingId(null);
|
||||
}
|
||||
@ -1298,9 +1304,9 @@ export default function UserReservationsPage() {
|
||||
/>
|
||||
<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>
|
||||
<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"/> {t('back')}</button>
|
||||
<h1 className="text-3xl font-bold text-gray-900 mb-2">{t('myBookings')}</h1>
|
||||
<p className="text-gray-600">{t('youHaveBookings', { count: 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]) => (
|
||||
@ -1308,20 +1314,20 @@ export default function UserReservationsPage() {
|
||||
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>
|
||||
<div className="text-sm text-gray-600">{s==='all'?t('all'):t(`bookingStatus.${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)}
|
||||
<input type="text" placeholder={t('searchPlaceholder')} 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>
|
||||
<h3 className="text-xl font-bold text-gray-900 mb-2">{t('noBookings')}</h3>
|
||||
<p className="text-gray-600">{t('noReservationsDesc')}</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
|
||||
Reference in New Issue
Block a user