forked from amer2004/SweetHome
Edit payment
This commit is contained in:
@ -26,7 +26,7 @@ import {
|
|||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import toast, { Toaster } from 'react-hot-toast';
|
import toast, { Toaster } from 'react-hot-toast';
|
||||||
import AuthService from '@/app/services/AuthService';
|
import AuthService from '@/app/services/AuthService';
|
||||||
import { payDeposit, getMyTransaction } from '@/app/utils/api';
|
import { payDeposit, getMyTransaction, getPaymentTypes } from '@/app/utils/api';
|
||||||
|
|
||||||
const STATUS_MAP = ['pending', 'ownerConfirmed', 'depositPaid', 'depositConfirmed', 'completed', 'cancelled'];
|
const STATUS_MAP = ['pending', 'ownerConfirmed', 'depositPaid', 'depositConfirmed', 'completed', 'cancelled'];
|
||||||
|
|
||||||
@ -67,6 +67,8 @@ export default function PaymentsPage() {
|
|||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [payingId, setPayingId] = useState(null);
|
const [payingId, setPayingId] = useState(null);
|
||||||
const [isGuest, setIsGuest] = useState(null);
|
const [isGuest, setIsGuest] = useState(null);
|
||||||
|
const [paymentMethods, setPaymentMethods] = useState([]);
|
||||||
|
const [loadingPaymentMethods, setLoadingPaymentMethods] = useState(true);
|
||||||
const [selectedPayment, setSelectedPayment] = useState('cash');
|
const [selectedPayment, setSelectedPayment] = useState('cash');
|
||||||
|
|
||||||
const loadReservations = useCallback(async () => {
|
const loadReservations = useCallback(async () => {
|
||||||
@ -109,6 +111,24 @@ export default function PaymentsPage() {
|
|||||||
}
|
}
|
||||||
}, [t]);
|
}, [t]);
|
||||||
|
|
||||||
|
const loadPaymentMethods = useCallback(async () => {
|
||||||
|
setLoadingPaymentMethods(true);
|
||||||
|
try {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[Payments] failed to load payment methods', err);
|
||||||
|
setPaymentMethods([]);
|
||||||
|
} finally {
|
||||||
|
setLoadingPaymentMethods(false);
|
||||||
|
}
|
||||||
|
}, [selectedPayment]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (AuthService.isGuest()) {
|
if (AuthService.isGuest()) {
|
||||||
setIsGuest(true);
|
setIsGuest(true);
|
||||||
@ -119,10 +139,25 @@ export default function PaymentsPage() {
|
|||||||
loadReservations();
|
loadReservations();
|
||||||
}, [loadReservations]);
|
}, [loadReservations]);
|
||||||
|
|
||||||
const handlePayDeposit = async (reservation) => {
|
useEffect(() => {
|
||||||
|
loadPaymentMethods();
|
||||||
|
}, [loadPaymentMethods]);
|
||||||
|
|
||||||
|
const resolvePaymentTypeId = (paymentKey) => {
|
||||||
|
const method = paymentMethods.find(
|
||||||
|
(m) => String(m.id) === String(paymentKey) || String(m.name) === String(paymentKey),
|
||||||
|
);
|
||||||
|
return method?.id ?? paymentKey;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePayDeposit = async (reservation, paymentKey) => {
|
||||||
setPayingId(reservation.id);
|
setPayingId(reservation.id);
|
||||||
|
const paymentTypeId = resolvePaymentTypeId(paymentKey);
|
||||||
try {
|
try {
|
||||||
await payDeposit({ reservationId: reservation.id });
|
await payDeposit({
|
||||||
|
reservationId: reservation.id,
|
||||||
|
paymentTypeId,
|
||||||
|
});
|
||||||
toast.success(t('payments.depositPaidSuccess'));
|
toast.success(t('payments.depositPaidSuccess'));
|
||||||
loadReservations();
|
loadReservations();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -196,6 +231,8 @@ export default function PaymentsPage() {
|
|||||||
key={r.id || i}
|
key={r.id || i}
|
||||||
reservation={r}
|
reservation={r}
|
||||||
payingId={payingId}
|
payingId={payingId}
|
||||||
|
paymentMethods={paymentMethods}
|
||||||
|
loadingPaymentMethods={loadingPaymentMethods}
|
||||||
selectedPayment={selectedPayment}
|
selectedPayment={selectedPayment}
|
||||||
onSelectPayment={setSelectedPayment}
|
onSelectPayment={setSelectedPayment}
|
||||||
onPay={handlePayDeposit}
|
onPay={handlePayDeposit}
|
||||||
@ -262,10 +299,11 @@ export default function PaymentsPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line react/prop-types
|
// eslint-disable-next-line react/prop-types
|
||||||
function PaymentCard({ reservation, payingId, selectedPayment, onSelectPayment, onPay }) {
|
function PaymentCard({ reservation, payingId, paymentMethods, loadingPaymentMethods, selectedPayment, onSelectPayment, onPay }) {
|
||||||
const r = reservation;
|
const r = reservation;
|
||||||
const amount = r.depositAmount || r.totalPrice || 0;
|
const amount = r.depositAmount || r.totalPrice || 0;
|
||||||
const [showCashDialog, setShowCashDialog] = useState(false);
|
const [showCashDialog, setShowCashDialog] = useState(false);
|
||||||
|
const methods = paymentMethods.length > 0 ? paymentMethods : getPaymentMethods(t);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -332,17 +370,29 @@ export default function PaymentsPage() {
|
|||||||
<div className="px-6 py-5 border-b border-gray-100">
|
<div className="px-6 py-5 border-b border-gray-100">
|
||||||
<p className="text-sm font-bold text-gray-700 mb-3">{t('payments.paymentMethodLabel')}</p>
|
<p className="text-sm font-bold text-gray-700 mb-3">{t('payments.paymentMethodLabel')}</p>
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||||
{getPaymentMethods(t).map((method) => {
|
{loadingPaymentMethods ? (
|
||||||
const isSelected = selectedPayment === method.id;
|
<div className="col-span-full rounded-2xl border border-dashed border-gray-200 bg-gray-50 p-4 text-sm text-gray-600">
|
||||||
const Icon = method.icon;
|
{t('payments.loadingPaymentMethods', { defaultValue: 'جاري تحميل طرق الدفع...' })}
|
||||||
|
</div>
|
||||||
|
) : methods.length === 0 ? (
|
||||||
|
<div className="col-span-full rounded-2xl border border-dashed border-gray-200 bg-gray-50 p-4 text-sm text-gray-600">
|
||||||
|
{t('payments.noPaymentMethodsAvailable', { defaultValue: 'لا توجد طرق دفع متاحة حالياً.' })}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
methods.map((method, index) => {
|
||||||
|
const optionId = method.id ?? method.name ?? `payment-method-${index}`;
|
||||||
|
const isActive = method?.isActive === true || method?.active === true;
|
||||||
|
const isSelected = String(selectedPayment) === String(optionId);
|
||||||
|
const Icon = method.icon || CreditCard;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={method.id}
|
key={optionId}
|
||||||
type="button"
|
type="button"
|
||||||
disabled={!method.active}
|
disabled={!isActive}
|
||||||
onClick={() => method.active && onSelectPayment(method.id)}
|
onClick={() => isActive && onSelectPayment(optionId)}
|
||||||
className={`relative flex items-start gap-3 p-4 rounded-2xl border-2 text-right transition-all ${
|
className={`relative flex items-start gap-3 p-4 rounded-2xl border-2 text-right transition-all ${
|
||||||
!method.active
|
!isActive
|
||||||
? 'border-gray-100 bg-gray-50 opacity-50 cursor-not-allowed'
|
? 'border-gray-100 bg-gray-50 opacity-50 cursor-not-allowed'
|
||||||
: isSelected
|
: isSelected
|
||||||
? 'border-amber-500 bg-amber-50 shadow-sm'
|
? 'border-amber-500 bg-amber-50 shadow-sm'
|
||||||
@ -356,16 +406,18 @@ export default function PaymentsPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<p className={`text-sm font-bold ${isSelected ? 'text-amber-900' : 'text-gray-800'}`}>
|
<p className={`text-sm font-bold ${isSelected ? 'text-amber-900' : 'text-gray-800'}`}>
|
||||||
{method.label}
|
{method.name || method.label || t('paymentMethod', { defaultValue: 'طريقة الدفع' })}
|
||||||
</p>
|
</p>
|
||||||
|
{method.description && (
|
||||||
<p className="text-xs text-gray-500 mt-0.5 leading-relaxed">
|
<p className="text-xs text-gray-500 mt-0.5 leading-relaxed">
|
||||||
{method.desc}
|
{method.description}
|
||||||
</p>
|
</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('validation.notAvailable', { defaultValue: 'غير متاح' })}
|
|
||||||
</span>
|
|
||||||
)}
|
)}
|
||||||
|
<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>
|
</div>
|
||||||
{isSelected && (
|
{isSelected && (
|
||||||
<div className="absolute top-2 left-2 w-5 h-5 bg-amber-500 rounded-full flex items-center justify-center">
|
<div className="absolute top-2 left-2 w-5 h-5 bg-amber-500 rounded-full flex items-center justify-center">
|
||||||
@ -374,7 +426,8 @@ export default function PaymentsPage() {
|
|||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
})}
|
})
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -382,7 +435,7 @@ export default function PaymentsPage() {
|
|||||||
<div className="px-6 py-5 border-b border-gray-100">
|
<div className="px-6 py-5 border-b border-gray-100">
|
||||||
<motion.button
|
<motion.button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => onPay(r)}
|
onClick={() => onPay(r, selectedPayment)}
|
||||||
disabled={payingId === r.id}
|
disabled={payingId === r.id}
|
||||||
whileHover={{ scale: 1.01 }}
|
whileHover={{ scale: 1.01 }}
|
||||||
whileTap={{ scale: 0.99 }}
|
whileTap={{ scale: 0.99 }}
|
||||||
|
|||||||
Reference in New Issue
Block a user