payment with haram

This commit is contained in:
Beilin-b
2026-07-11 05:21:38 -07:00
parent bb47b05858
commit c365503382
2 changed files with 210 additions and 135 deletions

View File

@ -61,6 +61,10 @@ function formatDate(date) {
return d.toLocaleDateString('en-GB'); return d.toLocaleDateString('en-GB');
} }
function normalizeText(value) {
return String(value ?? '').trim().toLowerCase();
}
export default function PaymentsPage() { export default function PaymentsPage() {
const { t, i18n } = useTranslation(); const { t, i18n } = useTranslation();
const [reservations, setReservations] = useState([]); const [reservations, setReservations] = useState([]);
@ -69,7 +73,7 @@ export default function PaymentsPage() {
const [isGuest, setIsGuest] = useState(null); const [isGuest, setIsGuest] = useState(null);
const [paymentMethods, setPaymentMethods] = useState([]); const [paymentMethods, setPaymentMethods] = useState([]);
const [loadingPaymentMethods, setLoadingPaymentMethods] = useState(true); const [loadingPaymentMethods, setLoadingPaymentMethods] = useState(true);
const [selectedPayment, setSelectedPayment] = useState('cash'); const [selectedPayment, setSelectedPayment] = useState(null);
const loadReservations = useCallback(async () => { const loadReservations = useCallback(async () => {
try { try {
@ -117,9 +121,10 @@ export default function PaymentsPage() {
const data = await getPaymentTypes(); const data = await getPaymentTypes();
const methods = Array.isArray(data) ? data : []; const methods = Array.isArray(data) ? data : [];
setPaymentMethods(methods); setPaymentMethods(methods);
const firstActive = methods.find((method) => method?.isActive === true || method?.active === true); const firstActive = methods.find((method) => method?.isActive === true || method?.active === true);
if (firstActive) { if (firstActive) {
setSelectedPayment(firstActive.id ?? firstActive.name ?? selectedPayment); setSelectedPayment(firstActive.name ?? firstActive.id ?? null);
} }
} catch (err) { } catch (err) {
console.error('[Payments] failed to load payment methods', err); console.error('[Payments] failed to load payment methods', err);
@ -127,7 +132,7 @@ export default function PaymentsPage() {
} finally { } finally {
setLoadingPaymentMethods(false); setLoadingPaymentMethods(false);
} }
}, [selectedPayment]); }, []);
useEffect(() => { useEffect(() => {
if (AuthService.isGuest()) { if (AuthService.isGuest()) {
@ -150,13 +155,25 @@ export default function PaymentsPage() {
return method?.id ?? paymentKey; return method?.id ?? paymentKey;
}; };
const handlePayDeposit = async (reservation, paymentKey) => { const handlePayDeposit = async (reservation, paymentKey, paymentImageFile) => {
setPayingId(reservation.id);
const paymentTypeId = resolvePaymentTypeId(paymentKey); const paymentTypeId = resolvePaymentTypeId(paymentKey);
const selectedMethod = paymentMethods.find(
(m) => String(m.id) === String(paymentTypeId) || String(m.name) === String(paymentTypeId),
);
const isHaram = normalizeText(selectedMethod?.name).includes('haram');
if (isHaram && !paymentImageFile) {
toast.error(t('payments.uploadReceiptRequired', { defaultValue: 'يرجى رفع صورة الوصل' }));
return;
}
setPayingId(reservation.id);
try { try {
await payDeposit({ await payDeposit({
reservationId: reservation.id, reservationId: reservation.id,
paymentTypeId, paymentTypeId,
paymentImage: paymentImageFile,
}); });
toast.success(t('payments.depositPaidSuccess')); toast.success(t('payments.depositPaidSuccess'));
loadReservations(); loadReservations();
@ -282,36 +299,54 @@ export default function PaymentsPage() {
</div> </div>
)} )}
{reservations.length === 0 && ( {reservations.length === 0 && (
<motion.div <motion.div
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
className="bg-white rounded-2xl p-12 text-center border-2 border-dashed border-gray-300" className="bg-white rounded-2xl p-12 text-center border-2 border-dashed border-gray-300"
> >
<CreditCard className="w-16 h-16 text-gray-300 mx-auto mb-4" /> <CreditCard className="w-16 h-16 text-gray-300 mx-auto mb-4" />
<h3 className="text-xl font-bold text-gray-700 mb-2">{t('payments.noTransactions')}</h3> <h3 className="text-xl font-bold text-gray-700 mb-2">{t('payments.noTransactions')}</h3>
<p className="text-gray-500">{t('payments.noTransactionsDesc')}</p> <p className="text-gray-500">{t('payments.noTransactionsDesc')}</p>
</motion.div> </motion.div>
)} )}
</div>
</div> </div>
); </div>
} );
}
// eslint-disable-next-line react/prop-types // eslint-disable-next-line react/prop-types
function PaymentCard({ reservation, payingId, paymentMethods, loadingPaymentMethods, selectedPayment, onSelectPayment, onPay }) { function PaymentCard({ reservation, payingId, paymentMethods, loadingPaymentMethods, selectedPayment, onSelectPayment, onPay }) {
const r = reservation; const { t } = useTranslation();
const amount = r.depositAmount || r.totalPrice || 0; const r = reservation;
const [showCashDialog, setShowCashDialog] = useState(false); const amount = r.depositAmount || r.totalPrice || 0;
const methods = paymentMethods.length > 0 ? paymentMethods : getPaymentMethods(t); const [showCashDialog, setShowCashDialog] = useState(false);
const [localPaymentImage, setLocalPaymentImage] = useState(null);
return ( const methods = paymentMethods.length > 0 ? paymentMethods : getPaymentMethods(t);
<>
<motion.div const selectedMethod = methods.find((m) => {
initial={{ opacity: 0, y: 20 }} const methodName = normalizeText(m?.name);
animate={{ opacity: 1, y: 0 }} const methodLabel = normalizeText(m?.label);
className="bg-white rounded-3xl shadow-md border border-gray-200 overflow-hidden" const methodId = normalizeText(m?.id);
> const paymentKey = normalizeText(selectedPayment);
return methodName === paymentKey || methodLabel === paymentKey || methodId === paymentKey;
});
const isHaramPayment =
normalizeText(selectedPayment).includes('haram') ||
normalizeText(selectedMethod?.name).includes('haram') ||
normalizeText(selectedMethod?.label).includes('haram') ||
normalizeText(selectedMethod?.id).includes('haram');
return (
<>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="bg-white rounded-3xl shadow-md border border-gray-200 overflow-hidden"
>
{/* Property details */} {/* Property details */}
<div className="p-6 border-b border-gray-100"> <div className="p-6 border-b border-gray-100">
<div className="flex items-start gap-4 mb-4"> <div className="flex items-start gap-4 mb-4">
@ -380,7 +415,7 @@ export default function PaymentsPage() {
</div> </div>
) : ( ) : (
methods.map((method, index) => { methods.map((method, index) => {
const optionId = method.id ?? method.name ?? `payment-method-${index}`; const optionId = method.name ?? method.id ?? `payment-method-${index}`;
const isActive = method?.isActive === true || method?.active === true; const isActive = method?.isActive === true || method?.active === true;
const isSelected = String(selectedPayment) === String(optionId); const isSelected = String(selectedPayment) === String(optionId);
const Icon = method.icon || CreditCard; const Icon = method.icon || CreditCard;
@ -399,9 +434,11 @@ export default function PaymentsPage() {
: 'border-gray-200 bg-white hover:border-amber-300 cursor-pointer' : 'border-gray-200 bg-white hover:border-amber-300 cursor-pointer'
}`} }`}
> >
<div className={`w-10 h-10 rounded-xl flex items-center justify-center shrink-0 ${ <div
isSelected ? 'bg-amber-500 text-white' : 'bg-gray-100 text-gray-500' className={`w-10 h-10 rounded-xl flex items-center justify-center shrink-0 ${
}`}> isSelected ? 'bg-amber-500 text-white' : 'bg-gray-100 text-gray-500'
}`}
>
<Icon className="w-5 h-5" /> <Icon className="w-5 h-5" />
</div> </div>
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
@ -413,9 +450,11 @@ export default function PaymentsPage() {
{method.description} {method.description}
</p> </p>
)} )}
<span className={`inline-block mt-1 text-[10px] font-medium px-2 py-0.5 rounded-full ${ <span
isActive ? 'bg-amber-100 text-amber-700' : 'bg-gray-200 text-gray-500' className={`inline-block mt-1 text-[10px] font-medium px-2 py-0.5 rounded-full ${
}`}> isActive ? 'bg-amber-100 text-amber-700' : 'bg-gray-200 text-gray-500'
}`}
>
{isActive ? t('active', { defaultValue: 'نشط' }) : t('inactive', { defaultValue: 'غير نشط' })} {isActive ? t('active', { defaultValue: 'نشط' }) : t('inactive', { defaultValue: 'غير نشط' })}
</span> </span>
</div> </div>
@ -431,103 +470,118 @@ export default function PaymentsPage() {
</div> </div>
</div> </div>
{/* Pay button */} {/* Upload receipt only for Haram */}
<div className="px-6 py-5 border-b border-gray-100"> {isHaramPayment && (
<motion.button <div className="px-6 py-5 border-b border-gray-100">
type="button" <label className="block text-sm font-bold text-gray-700 mb-3">
onClick={() => onPay(r, selectedPayment)} {t('payments.uploadReceipt', { defaultValue: 'صورة الوصل' })}
disabled={payingId === r.id} </label>
whileHover={{ scale: 1.01 }} <input
whileTap={{ scale: 0.99 }} type="file"
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" accept="image/*"
> onChange={(e) => setLocalPaymentImage(e.target.files?.[0] || null)}
{payingId === r.id ? ( className="block w-full rounded-2xl border border-gray-300 bg-white px-4 py-3 text-sm text-gray-700 file:mr-4 file:rounded-xl file:border-0 file:bg-amber-500 file:px-4 file:py-2 file:text-white hover:file:bg-amber-600"
<> />
<Loader2 className="w-5 h-5 animate-spin" /> </div>
{t('payments.processingPayment')} )}
</>
) : (
<>
<Banknote className="w-5 h-5" />
{t('payments.payButton', { amount: formatCurrency(amount, r.currencySign) })}
</>
)}
</motion.button>
</div>
{/* Cash pay button */} {/* Pay button */}
<div className="px-6 py-4 border-b border-gray-100"> <div className="px-6 py-5 border-b border-gray-100">
<button <motion.button
type="button" type="button"
onClick={() => setShowCashDialog(true)} onClick={() => onPay(r, selectedPayment, localPaymentImage)}
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" disabled={payingId === r.id}
> whileHover={{ scale: 1.01 }}
<Building className="w-5 h-5" /> whileTap={{ scale: 0.99 }}
{t('payments.payCashButton')} 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"
</button>
</div>
{/* Cash payment dialog */}
{showCashDialog && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 px-4 py-8">
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
className="w-full max-w-md rounded-3xl bg-white p-8 shadow-2xl border border-gray-200 text-center"
> >
<div className="w-16 h-16 bg-amber-100 rounded-full flex items-center justify-center mx-auto mb-5"> {payingId === r.id ? (
<Clock className="w-8 h-8 text-amber-600" /> <>
</div> <Loader2 className="w-5 h-5 animate-spin" />
<h3 className="text-xl font-bold text-gray-900 mb-3">{t('payments.cashPendingDialogTitle')}</h3> {t('payments.processingPayment')}
<p className="text-gray-600 leading-relaxed mb-6"> </>
{t('payments.cashPendingDialogDesc1')} ) : (
<br /> <>
{t('payments.cashPendingDialogDesc2')} <Banknote className="w-5 h-5" />
</p> {t('payments.payButton', { amount: formatCurrency(amount, r.currencySign) })}
<div className="bg-gray-50 rounded-2xl p-4 mb-6 text-right"> </>
<p className="text-sm font-bold text-gray-800 mb-1">{t('payments.platformOfficeLabel')}</p> )}
<p className="text-sm text-gray-600">{t('payments.platformOfficeFullAddress')}</p> </motion.button>
<p className="text-sm text-gray-600 flex items-center gap-1 mt-1" dir="ltr"> </div>
{/* Cash pay button */}
<div className="px-6 py-4 border-b border-gray-100">
<button
type="button"
onClick={() => setShowCashDialog(true)}
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('payments.payCashButton')}
</button>
</div>
{/* Cash payment dialog */}
{showCashDialog && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 px-4 py-8">
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
className="w-full max-w-md rounded-3xl bg-white p-8 shadow-2xl border border-gray-200 text-center"
>
<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">{t('payments.cashPendingDialogTitle')}</h3>
<p className="text-gray-600 leading-relaxed mb-6">
{t('payments.cashPendingDialogDesc1')}
<br />
{t('payments.cashPendingDialogDesc2')}
</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">{t('payments.platformOfficeLabel')}</p>
<p className="text-sm text-gray-600">{t('payments.platformOfficeFullAddress')}</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
</p>
</div>
<div className="flex flex-col gap-3 sm:flex-row">
<button
type="button"
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('payments.closeButton')}
</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('payments.myReservationsLink')}
</Link>
</div>
</motion.div>
</div>
)}
{/* Platform location */}
<div className="px-6 py-5 bg-gray-50">
<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">{t('payments.cashPaymentLocationLabel')}</p>
<p className="text-sm text-gray-600 leading-relaxed">
{t('payments.platformOfficeFullAddress')}
</p>
<p className="text-sm text-gray-600 flex items-center gap-1 mt-1">
<Phone className="w-3.5 h-3.5" /> <Phone className="w-3.5 h-3.5" />
+963567823411 <span dir="ltr">+963567823411</span>
</p> </p>
</div> </div>
<div className="flex flex-col gap-3 sm:flex-row">
<button
type="button"
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('payments.closeButton')}
</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('payments.myReservationsLink')}
</Link>
</div>
</motion.div>
</div>
)}
{/* Platform location */}
<div className="px-6 py-5 bg-gray-50">
<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">{t('payments.cashPaymentLocationLabel')}</p>
<p className="text-sm text-gray-600 leading-relaxed">
{t('payments.platformOfficeFullAddress')}
</p>
<p className="text-sm text-gray-600 flex items-center gap-1 mt-1">
<Phone className="w-3.5 h-3.5" />
<span dir="ltr">+963567823411</span>
</p>
</div> </div>
</div> </div>
</div> </motion.div>
</motion.div>
</> </>
); );
} }

View File

@ -672,17 +672,38 @@ export async function ownerConfirmReservation(id) {
// ─── Payments ─── // ─── Payments ───
export async function payDeposit(data) { export async function payDeposit(data) {
const formData = new FormData();
formData.append(
"ReservationId",
String(data.reservationId ?? data.ReservationId ?? "")
);
formData.append(
"PaymentTypeId",
String(data.paymentTypeId ?? data.PaymentTypeId ?? "")
);
if (data.comment) {
formData.append("Comment", data.comment);
} else {
formData.append("Comment", "");
}
// صورة الوصل لطريقة الدفع Haram
if (data.paymentImage) {
formData.append("TransactionType", data.paymentImage);
formData.append("paymentImage", data.paymentImage);
}
return apiFetch("/Reservations/PayDeposit/pay-deposit", { return apiFetch("/Reservations/PayDeposit/pay-deposit", {
method: "POST", method: "POST",
body: data, body: formData,
}); });
} }
export async function getMyTransaction() {
return apiFetch("/Customer/GetMyTransaction");
}
// ─── Owner Contact & Stats ─── // ─── Owner Contact & Stats ───
export async function getOwnerContactInformation(propertyInformationId) { export async function getOwnerContactInformation(propertyInformationId) {