diff --git a/app/reservations/page.js b/app/reservations/page.js index 9265475..02cda3f 100644 --- a/app/reservations/page.js +++ b/app/reservations/page.js @@ -388,15 +388,13 @@ // ); // } +"use client"; - -'use client'; - -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 { 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, @@ -422,74 +420,97 @@ import { Landmark, Phone, Wallet, -} from 'lucide-react'; -import toast, { Toaster } from 'react-hot-toast'; -import AuthService from '../services/AuthService'; -import { getPaymentTypes, getRentProperties, getUserReservations, payDeposit } from '../utils/api'; -import { addPropertyRating } from '../utils/ratings'; + ImageIcon, +} from "lucide-react"; +import toast, { Toaster } from "react-hot-toast"; +import AuthService from "../services/AuthService"; +import { + getPaymentTypes, + getRentProperties, + getUserReservations, + payDeposit, +} from "../utils/api"; +import { addPropertyRating } from "../utils/ratings"; -const API_BASE = process.env.NEXT_PUBLIC_API_URL || 'https://45.93.137.91.nip.io/api'; +const 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: { 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 }, + 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 }, }; -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 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()}`; } function formatDate(date) { - if (!date) return ''; + if (!date) return ""; const d = new Date(date); - if (Number.isNaN(d.getTime())) return ''; - return d.toLocaleDateString('en-GB'); + if (Number.isNaN(d.getTime())) return ""; + return d.toLocaleDateString("en-GB"); } function StatusBadge({ code }) { const { t } = useTranslation(); const Icon = statusIcon(code); - const key = STATUS_MAP[code] || 'pending'; + const key = STATUS_MAP[code] || "pending"; return ( - + {t(`bookingStatus.${key}`)} ); } -const propAddr = (p, r) => p?.address ?? r?.propertyAddress ?? ''; +const propAddr = (p, r) => p?.address ?? r?.propertyAddress ?? ""; const propImages = (p, r) => { if (p?.images && Array.isArray(p.images)) return p.images; - if (r?.property?.images && Array.isArray(r.property.images)) return r.property.images; + if (r?.property?.images && Array.isArray(r.property.images)) + return r.property.images; return []; }; -const propBeds = (p, r) => p?.numberOfBedRooms ?? r?.property?.numberOfBedRooms ?? 0; -const propBaths = (p, r) => p?.numberOfBathRooms ?? r?.property?.numberOfBathRooms ?? 0; +const propBeds = (p, r) => + p?.numberOfBedRooms ?? r?.property?.numberOfBedRooms ?? 0; +const propBaths = (p, r) => + p?.numberOfBathRooms ?? r?.property?.numberOfBathRooms ?? 0; const getAuthToken = () => { - if (typeof window === 'undefined') return ''; + if (typeof window === "undefined") return ""; return ( AuthService.getToken?.() || AuthService.getAccessToken?.() || - localStorage.getItem('token') || - localStorage.getItem('accessToken') || - localStorage.getItem('authToken') || - '' + localStorage.getItem("token") || + localStorage.getItem("accessToken") || + localStorage.getItem("authToken") || + "" ); }; const readStoredUser = () => { - if (typeof window === 'undefined') return null; - const keys = ['user', 'currentUser', 'authUser', 'profile']; + if (typeof window === "undefined") return null; + const keys = ["user", "currentUser", "authUser", "profile"]; for (const key of keys) { const raw = localStorage.getItem(key); if (!raw) continue; @@ -504,12 +525,12 @@ const readStoredUser = () => { const extractNumericUserId = (value) => { if (!value) return null; - if (typeof value === 'number') return Number.isInteger(value) ? value : null; - if (typeof value === 'string') { + if (typeof value === "number") return Number.isInteger(value) ? value : null; + if (typeof value === "string") { const n = Number(value); return Number.isInteger(n) ? n : null; } - if (typeof value === 'object') { + if (typeof value === "object") { const candidates = [ value.id, value.userId, @@ -534,17 +555,17 @@ async function reportReservation(reservationId, message) { const rid = Number(reservationId); if (!Number.isInteger(rid)) { - throw new Error('Invalid reservation ID'); + throw new Error("Invalid reservation ID"); } if (!Number.isInteger(reporter)) { - throw new Error('Could not identify current user'); + throw new Error("Could not identify current user"); } const token = getAuthToken(); const res = await fetch(`${API_BASE}/ReservationReports/ReportReservation`, { - method: 'POST', + method: "POST", headers: { - 'Content-Type': 'application/json', + "Content-Type": "application/json", ...(token ? { Authorization: `Bearer ${token}` } : {}), }, body: JSON.stringify({ @@ -555,7 +576,7 @@ async function reportReservation(reservationId, message) { }); if (!res.ok) { - let errorMessage = 'Failed to submit report'; + let errorMessage = "Failed to submit report"; try { const data = await res.json(); errorMessage = data?.message || data?.title || errorMessage; @@ -577,42 +598,46 @@ async function reportReservation(reservationId, message) { function parseTimeSpan(str) { if (!str) return 0; - const clean = str.replace(/-/g, ''); - const dotIdx = clean.indexOf('.'); - let days = 0, timePart = clean; + const clean = str.replace(/-/g, ""); + const dotIdx = clean.indexOf("."); + let days = 0, + timePart = clean; if (dotIdx !== -1) { days = parseInt(clean.substring(0, dotIdx), 10) || 0; timePart = clean.substring(dotIdx + 1); } - const parts = timePart.split(':'); + const parts = timePart.split(":"); if (parts.length < 2) return days * 86400000; const hh = parseInt(parts[0], 10) || 0; const mm = parseInt(parts[1], 10) || 0; - const ss = parts.length > 2 ? (parseInt(parts[2], 10) || 0) : 0; - return ((days * 86400) + (hh * 3600) + (mm * 60) + ss) * 1000; + const ss = parts.length > 2 ? parseInt(parts[2], 10) || 0 : 0; + return (days * 86400 + hh * 3600 + mm * 60 + ss) * 1000; } function formatWindowDuration(str) { - if (!str) return ''; - const clean = str.replace(/-/g, ''); - const dotIdx = clean.indexOf('.'); - let totalHours = 0, timePart = clean; + if (!str) return ""; + const clean = str.replace(/-/g, ""); + const dotIdx = clean.indexOf("."); + let totalHours = 0, + timePart = clean; if (dotIdx !== -1) { const days = parseInt(clean.substring(0, dotIdx), 10) || 0; totalHours += days * 24; timePart = clean.substring(dotIdx + 1); } - const parts = timePart.split(':'); + const parts = timePart.split(":"); if (parts.length >= 2) { totalHours += parseInt(parts[0], 10) || 0; } - if (totalHours > 0) return `${String(totalHours).padStart(2, '0')}:00:00`; + if (totalHours > 0) return `${String(totalHours).padStart(2, "0")}:00:00`; return timePart.substring(0, 8); } function CountdownTimer({ deadline }) { const { t } = useTranslation(); - const [remaining, setRemaining] = useState(deadline ? Math.max(0, deadline - Date.now()) : 0); + const [remaining, setRemaining] = useState( + deadline ? Math.max(0, deadline - Date.now()) : 0, + ); useEffect(() => { if (!deadline) return; const tick = () => setRemaining(Math.max(0, deadline - Date.now())); @@ -620,12 +645,21 @@ function CountdownTimer({ deadline }) { const id = setInterval(tick, 1000); return () => clearInterval(id); }, [deadline]); - if (remaining <= 0) return {t('timeExpired')}; + if (remaining <= 0) + return ( + + {t("timeExpired")} + + ); const h = Math.floor(remaining / 3600000); const m = Math.floor((remaining % 3600000) / 60000); const s = Math.floor((remaining % 60000) / 1000); - const pad = (n) => String(n).padStart(2, '0'); - return {pad(h)}:{pad(m)}:{pad(s)}; + const pad = (n) => String(n).padStart(2, "0"); + return ( + + {pad(h)}:{pad(m)}:{pad(s)} + + ); } function PaymentDialog({ @@ -638,6 +672,8 @@ function PaymentDialog({ payingId, paymentMethods, loadingPaymentMethods, + receiptImage, + onSetReceiptImage, }) { const { t } = useTranslation(); const [showCashDialog, setShowCashDialog] = useState(false); @@ -649,7 +685,12 @@ function PaymentDialog({ if (!isOpen || !reservation) return null; const amount = reservation.depositAmount || reservation.totalPrice || 0; - const currencySign = reservation.currencySign || t('sypSymbol'); + const currencySign = reservation.currencySign || t("sypSymbol"); + const selectedMethod = paymentMethods?.find( + (m) => (m?.id ?? m?.name) === selectedPayment + ); + const isHaram = selectedMethod?.name?.toLowerCase?.() === "haram"; + const canPay = payingId !== reservation.id && (!isHaram || !!receiptImage); return ( <> @@ -675,7 +716,9 @@ function PaymentDialog({ {(reservation.propertyAddress || reservation.propertyCity) && (

- {[reservation.propertyCity, reservation.propertyAddress].filter(Boolean).join(' - ')} + {[reservation.propertyCity, reservation.propertyAddress] + .filter(Boolean) + .join(" - ")}

)} @@ -684,52 +727,64 @@ function PaymentDialog({
- {formatDate(reservation.startDate)} - {formatDate(reservation.endDate)} + {formatDate(reservation.startDate)} -{" "} + {formatDate(reservation.endDate)} - - #{reservation.reservationId || reservation.id} + # + {reservation.reservationId || reservation.id}
-

{t('depositAmount')}

+

{t("depositAmount")}

{formatCurrency(amount, currencySign)}

- {t('depositPaidToPlatform')} + {t("depositPaidToPlatform")}

- -
+ {/* this disabled becuase if all of them not active or active there is no manual*/} + {/*

{t('cashOnlyNotice')}

-
+
*/}
-

{t('paymentMethod')}

+

+ {t("paymentMethod")} +

{loadingPaymentMethods ? (
- {t('loadingPaymentMethods', { defaultValue: 'جاري تحميل طرق الدفع...' })} + {t("loadingPaymentMethods", { + defaultValue: "جاري تحميل طرق الدفع...", + })}
- ) : (!Array.isArray(paymentMethods) || paymentMethods.length === 0) ? ( + ) : !Array.isArray(paymentMethods) || + paymentMethods.length === 0 ? (
- {t('noPaymentMethodsAvailable', { defaultValue: 'لا توجد طرق دفع متاحة حالياً.' })} + {t("noPaymentMethodsAvailable", { + defaultValue: "لا توجد طرق دفع متاحة حالياً.", + })}
) : (
{paymentMethods.map((method, index) => { - const isActive = method?.isActive === true || method?.active === true; - const optionId = method?.id ?? method?.name ?? `payment-method-${index}`; - const label = method?.name || t('paymentMethod', { defaultValue: 'طريقة الدفع' }); + const isActive = + method?.isActive === true || method?.active === true; + const optionId = + method?.id ?? method?.name ?? `payment-method-${index}`; + const label = + method?.name || + t("paymentMethod", { defaultValue: "طريقة الدفع" }); const isSelected = selectedPayment === optionId; return ( @@ -740,30 +795,50 @@ function PaymentDialog({ onClick={() => isActive && onSelectPayment(optionId)} className={`relative flex items-start gap-3 p-4 rounded-2xl border-2 text-right transition-all ${ !isActive - ? 'border-gray-100 bg-gray-50 opacity-50 cursor-not-allowed' + ? "border-gray-100 bg-gray-50 opacity-50 cursor-not-allowed" : isSelected - ? 'border-amber-500 bg-amber-50 shadow-sm' - : 'border-gray-200 bg-white hover:border-amber-300 cursor-pointer' + ? "border-amber-500 bg-amber-50 shadow-sm" + : "border-gray-200 bg-white hover:border-amber-300 cursor-pointer" }`} > -
- {isActive ? : } +
+ {isActive ? ( + + ) : ( + + )}
-

+

{label}

{isActive - ? t('paymentMethodAvailable', { defaultValue: 'متاحة للاستعمال' }) - : t('paymentMethodUnavailable', { defaultValue: 'غير متاحة حالياً' })} + ? t("paymentMethodAvailable", { + defaultValue: "متاحة للاستعمال", + }) + : t("paymentMethodUnavailable", { + defaultValue: "غير متاحة حالياً", + })}

- - {isActive ? t('active', { defaultValue: 'نشطة' }) : t('inactive', { defaultValue: 'غير نشطة' })} + + {isActive + ? t("active", { defaultValue: "نشطة ✅" }) + : t("inactive", { defaultValue: "غير نشطة ❌" })}
{isSelected && ( @@ -778,11 +853,63 @@ function PaymentDialog({ )}
+ {isHaram && ( +
+

+ + {t("uploadReceipt", { defaultValue: "إيصال الدفع" })} +

+ {receiptImage ? ( +
+ receipt + +
+

{receiptImage.name}

+
+
+ ) : ( + + )} +
+ )} +
onConfirmPay(reservation)} - disabled={payingId === reservation.id} + disabled={!canPay} whileHover={{ scale: 1.01 }} whileTap={{ scale: 0.99 }} className="w-full bg-amber-500 hover:bg-amber-600 disabled:bg-amber-300 text-white font-bold py-4 px-6 rounded-2xl text-lg transition-all shadow-lg hover:shadow-xl flex items-center justify-center gap-3" @@ -790,35 +917,41 @@ function PaymentDialog({ {payingId === reservation.id ? ( <> - {t('processingPayment')} + {t("processingPayment")} ) : ( <> - {t('payDeposit', { amount: formatCurrency(amount, currencySign) })} + {t("payDeposit", { + amount: formatCurrency(amount, currencySign), + })} )}
-
+ {/* Pay Cash button - commented out */} + {/*
-
+
*/} -
+ {/* Cash payment location - commented out */} + {/*
-

{t('cashPaymentLocation')}

+

+ {t("cashPaymentLocation")} +

- {t('platformOfficeAddress')} + {t("platformOfficeAddress")}

@@ -826,7 +959,7 @@ function PaymentDialog({

-
+
*/}
@@ -850,14 +983,23 @@ function PaymentDialog({
-

{t('cashPaymentPending')}

+

+ {t("cashPaymentPending")} +

- {t('cashPaymentInstructions')} + {t("cashPaymentInstructions")}

-

{t('platformLocation')}

-

{t('platformOfficeAddress')}

-

+

+ {t("platformLocation")} +

+

+ {t("platformOfficeAddress")} +

+

+963567823411

@@ -868,13 +1010,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')} + {t("close")} - {t('myBookings')} + {t("myBookings")}
@@ -886,10 +1028,10 @@ function PaymentDialog({ function ReportDialog({ isOpen, reservation, onClose, onSubmit, submitting }) { const { t } = useTranslation(); - const [message, setMessage] = useState(''); + const [message, setMessage] = useState(""); useEffect(() => { - if (isOpen) setMessage(''); + if (isOpen) setMessage(""); }, [isOpen, reservation?.id]); if (!isOpen || !reservation) return null; @@ -912,10 +1054,15 @@ function ReportDialog({ isOpen, reservation, onClose, onSubmit, submitting }) {
-

{t('reportReservation')}

-

{t('reservationNumber', { id: reservation.id })}

+

{t("reportReservation")}

+

+ {t("reservationNumber", { id: reservation.id })} +

-
@@ -923,13 +1070,13 @@ function ReportDialog({ isOpen, reservation, onClose, onSubmit, submitting }) {

- {t('reportDescription')} + {t("reportDescription")}