payment with haram
Some checks failed
Build frontend / build (push) Failing after 1m39s

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');
}
function normalizeText(value) {
return String(value ?? '').trim().toLowerCase();
}
export default function PaymentsPage() {
const { t, i18n } = useTranslation();
const [reservations, setReservations] = useState([]);
@ -69,7 +73,7 @@ export default function PaymentsPage() {
const [isGuest, setIsGuest] = useState(null);
const [paymentMethods, setPaymentMethods] = useState([]);
const [loadingPaymentMethods, setLoadingPaymentMethods] = useState(true);
const [selectedPayment, setSelectedPayment] = useState('cash');
const [selectedPayment, setSelectedPayment] = useState(null);
const loadReservations = useCallback(async () => {
try {
@ -117,9 +121,10 @@ export default function PaymentsPage() {
const data = await getPaymentTypes();
const methods = Array.isArray(data) ? data : [];
setPaymentMethods(methods);
const firstActive = methods.find((method) => method?.isActive === true || method?.active === true);
if (firstActive) {
setSelectedPayment(firstActive.id ?? firstActive.name ?? selectedPayment);
setSelectedPayment(firstActive.name ?? firstActive.id ?? null);
}
} catch (err) {
console.error('[Payments] failed to load payment methods', err);
@ -127,7 +132,7 @@ export default function PaymentsPage() {
} finally {
setLoadingPaymentMethods(false);
}
}, [selectedPayment]);
}, []);
useEffect(() => {
if (AuthService.isGuest()) {
@ -150,13 +155,25 @@ export default function PaymentsPage() {
return method?.id ?? paymentKey;
};
const handlePayDeposit = async (reservation, paymentKey) => {
setPayingId(reservation.id);
const handlePayDeposit = async (reservation, paymentKey, paymentImageFile) => {
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 {
await payDeposit({
reservationId: reservation.id,
paymentTypeId,
paymentImage: paymentImageFile,
});
toast.success(t('payments.depositPaidSuccess'));
loadReservations();
@ -282,36 +299,54 @@ export default function PaymentsPage() {
</div>
)}
{reservations.length === 0 && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
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" />
<h3 className="text-xl font-bold text-gray-700 mb-2">{t('payments.noTransactions')}</h3>
<p className="text-gray-500">{t('payments.noTransactionsDesc')}</p>
</motion.div>
)}
</div>
{reservations.length === 0 && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
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" />
<h3 className="text-xl font-bold text-gray-700 mb-2">{t('payments.noTransactions')}</h3>
<p className="text-gray-500">{t('payments.noTransactionsDesc')}</p>
</motion.div>
)}
</div>
);
}
</div>
);
}
// eslint-disable-next-line react/prop-types
function PaymentCard({ reservation, payingId, paymentMethods, loadingPaymentMethods, selectedPayment, onSelectPayment, onPay }) {
const r = reservation;
const amount = r.depositAmount || r.totalPrice || 0;
const [showCashDialog, setShowCashDialog] = useState(false);
const methods = paymentMethods.length > 0 ? paymentMethods : getPaymentMethods(t);
// eslint-disable-next-line react/prop-types
function PaymentCard({ reservation, payingId, paymentMethods, loadingPaymentMethods, selectedPayment, onSelectPayment, onPay }) {
const { t } = useTranslation();
const r = reservation;
const amount = r.depositAmount || r.totalPrice || 0;
const [showCashDialog, setShowCashDialog] = useState(false);
const [localPaymentImage, setLocalPaymentImage] = useState(null);
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"
>
const methods = paymentMethods.length > 0 ? paymentMethods : getPaymentMethods(t);
const selectedMethod = methods.find((m) => {
const methodName = normalizeText(m?.name);
const methodLabel = normalizeText(m?.label);
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 */}
<div className="p-6 border-b border-gray-100">
<div className="flex items-start gap-4 mb-4">
@ -380,7 +415,7 @@ export default function PaymentsPage() {
</div>
) : (
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 isSelected = String(selectedPayment) === String(optionId);
const Icon = method.icon || CreditCard;
@ -399,9 +434,11 @@ export default function PaymentsPage() {
: '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 ${
isSelected ? 'bg-amber-500 text-white' : 'bg-gray-100 text-gray-500'
}`}>
<div
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" />
</div>
<div className="min-w-0 flex-1">
@ -413,9 +450,11 @@ export default function PaymentsPage() {
{method.description}
</p>
)}
<span 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'
}`}>
<span
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: 'غير نشط' })}
</span>
</div>
@ -431,103 +470,118 @@ export default function PaymentsPage() {
</div>
</div>
{/* Pay button */}
<div className="px-6 py-5 border-b border-gray-100">
<motion.button
type="button"
onClick={() => onPay(r, selectedPayment)}
disabled={payingId === r.id}
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"
>
{payingId === r.id ? (
<>
<Loader2 className="w-5 h-5 animate-spin" />
{t('payments.processingPayment')}
</>
) : (
<>
<Banknote className="w-5 h-5" />
{t('payments.payButton', { amount: formatCurrency(amount, r.currencySign) })}
</>
)}
</motion.button>
</div>
{/* Upload receipt only for Haram */}
{isHaramPayment && (
<div className="px-6 py-5 border-b border-gray-100">
<label className="block text-sm font-bold text-gray-700 mb-3">
{t('payments.uploadReceipt', { defaultValue: 'صورة الوصل' })}
</label>
<input
type="file"
accept="image/*"
onChange={(e) => setLocalPaymentImage(e.target.files?.[0] || null)}
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"
/>
</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"
{/* Pay button */}
<div className="px-6 py-5 border-b border-gray-100">
<motion.button
type="button"
onClick={() => onPay(r, selectedPayment, localPaymentImage)}
disabled={payingId === r.id}
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"
>
<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">
{payingId === r.id ? (
<>
<Loader2 className="w-5 h-5 animate-spin" />
{t('payments.processingPayment')}
</>
) : (
<>
<Banknote className="w-5 h-5" />
{t('payments.payButton', { amount: formatCurrency(amount, r.currencySign) })}
</>
)}
</motion.button>
</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" />
+963567823411
<span dir="ltr">+963567823411</span>
</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" />
<span dir="ltr">+963567823411</span>
</p>
</div>
</div>
</div>
</motion.div>
</motion.div>
</>
);
}
}

View File

@ -672,17 +672,38 @@ export async function ownerConfirmReservation(id) {
// ─── Payments ───
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", {
method: "POST",
body: data,
body: formData,
});
}
export async function getMyTransaction() {
return apiFetch("/Customer/GetMyTransaction");
}
// ─── Owner Contact & Stats ───
export async function getOwnerContactInformation(propertyInformationId) {