forked from amer2004/SweetHome
Added payment types
This commit is contained in:
@ -187,7 +187,7 @@ function mapApiDetail(item) {
|
||||
const rawImages = Array.isArray(info.images) ? info.images : [];
|
||||
const photosFallback = Array.isArray(details.photos) ? details.photos : [];
|
||||
const allRaw = rawImages.length > 0 ? rawImages : photosFallback;
|
||||
const images = allRaw.length > 0 ? allRaw.map(buildImageUrl) : [];
|
||||
const images = allRaw.length > 0 ? allRaw.map(buildImageUrl).filter(Boolean) : [];
|
||||
|
||||
// Normalize services: Flutter sends list of strings or object with boolean values
|
||||
const rawServices = details.services || {};
|
||||
|
||||
@ -425,7 +425,7 @@ import {
|
||||
} from 'lucide-react';
|
||||
import toast, { Toaster } from 'react-hot-toast';
|
||||
import AuthService from '../services/AuthService';
|
||||
import { getRentProperties, getUserReservations, payDeposit } from '../utils/api';
|
||||
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';
|
||||
@ -441,37 +441,6 @@ const STATUS_UI = {
|
||||
cancelled: { color: 'bg-gray-100 text-gray-800', icon: XCircle },
|
||||
};
|
||||
|
||||
const PAYMENT_METHODS = [
|
||||
{
|
||||
id: 'cash',
|
||||
labelKey: 'payment.cash',
|
||||
descKey: 'payment.cashDesc',
|
||||
icon: Banknote,
|
||||
active: true,
|
||||
},
|
||||
{
|
||||
id: 'office',
|
||||
labelKey: 'payment.office',
|
||||
descKey: 'payment.officeDesc',
|
||||
icon: Building,
|
||||
active: true,
|
||||
},
|
||||
{
|
||||
id: 'transfer',
|
||||
labelKey: 'payment.transfer',
|
||||
descKey: 'payment.transferDesc',
|
||||
icon: ArrowLeftRight,
|
||||
active: false,
|
||||
},
|
||||
{
|
||||
id: 'electronic',
|
||||
labelKey: 'payment.electronic',
|
||||
descKey: 'payment.electronicDesc',
|
||||
icon: CreditCard,
|
||||
active: false,
|
||||
},
|
||||
];
|
||||
|
||||
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; }
|
||||
|
||||
@ -667,6 +636,8 @@ function PaymentDialog({
|
||||
onSelectPayment,
|
||||
onConfirmPay,
|
||||
payingId,
|
||||
paymentMethods,
|
||||
loadingPaymentMethods,
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [showCashDialog, setShowCashDialog] = useState(false);
|
||||
@ -745,52 +716,66 @@ function PaymentDialog({
|
||||
|
||||
<div className="px-6 py-5 border-b border-gray-100">
|
||||
<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;
|
||||
const Icon = method.icon;
|
||||
{loadingPaymentMethods ? (
|
||||
<div className="rounded-2xl border border-dashed border-gray-200 bg-gray-50 p-4 text-sm text-gray-600">
|
||||
{t('loadingPaymentMethods', { defaultValue: 'جاري تحميل طرق الدفع...' })}
|
||||
</div>
|
||||
) : (!Array.isArray(paymentMethods) || paymentMethods.length === 0) ? (
|
||||
<div className="rounded-2xl border border-dashed border-gray-200 bg-gray-50 p-4 text-sm text-gray-600">
|
||||
{t('noPaymentMethodsAvailable', { defaultValue: 'لا توجد طرق دفع متاحة حالياً.' })}
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
{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 isSelected = selectedPayment === optionId;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={method.id}
|
||||
type="button"
|
||||
disabled={!method.active}
|
||||
onClick={() => method.active && onSelectPayment(method.id)}
|
||||
className={`relative flex items-start gap-3 p-4 rounded-2xl border-2 text-right transition-all ${
|
||||
!method.active
|
||||
? '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'
|
||||
}`}
|
||||
>
|
||||
<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">
|
||||
<p className={`text-sm font-bold ${isSelected ? 'text-amber-900' : 'text-gray-800'}`}>
|
||||
{t(method.labelKey)}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 mt-0.5 leading-relaxed">
|
||||
{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>
|
||||
{isSelected && (
|
||||
<div className="absolute top-2 left-2 w-5 h-5 bg-amber-500 rounded-full flex items-center justify-center">
|
||||
<Check className="w-3 h-3 text-white" />
|
||||
return (
|
||||
<button
|
||||
key={optionId}
|
||||
type="button"
|
||||
disabled={!isActive}
|
||||
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'
|
||||
: isSelected
|
||||
? 'border-amber-500 bg-amber-50 shadow-sm'
|
||||
: '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'
|
||||
}`}>
|
||||
{isActive ? <CreditCard className="w-5 h-5" /> : <X className="w-5 h-5" />}
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className={`text-sm font-bold ${isSelected ? 'text-amber-900' : 'text-gray-800'}`}>
|
||||
{label}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 mt-0.5 leading-relaxed">
|
||||
{isActive
|
||||
? t('paymentMethodAvailable', { defaultValue: 'متاحة للاستعمال' })
|
||||
: t('paymentMethodUnavailable', { defaultValue: 'غير متاحة حالياً' })}
|
||||
</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'
|
||||
}`}>
|
||||
{isActive ? t('active', { defaultValue: 'نشطة' }) : t('inactive', { defaultValue: 'غير نشطة' })}
|
||||
</span>
|
||||
</div>
|
||||
{isSelected && (
|
||||
<div className="absolute top-2 left-2 w-5 h-5 bg-amber-500 rounded-full flex items-center justify-center">
|
||||
<Check className="w-3 h-3 text-white" />
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="px-6 py-5 border-b border-gray-100">
|
||||
@ -1176,8 +1161,8 @@ export default function UserReservationsPage() {
|
||||
const [reportingId, setReportingId] = useState(null);
|
||||
const [selectedPayment, setSelectedPayment] = useState('cash');
|
||||
const [paymentDialog, setPaymentDialog] = useState({ open: false, reservation: null });
|
||||
|
||||
useEffect(() => { if (!AuthService.getUser()) { router.push('/login'); return; } loadReservations(); }, [router]);
|
||||
const [paymentMethods, setPaymentMethods] = useState([]);
|
||||
const [loadingPaymentMethods, setLoadingPaymentMethods] = useState(false);
|
||||
|
||||
const loadReservations = useCallback(async () => {
|
||||
try {
|
||||
@ -1207,8 +1192,27 @@ export default function UserReservationsPage() {
|
||||
setFiltered([]);
|
||||
}
|
||||
setLoading(false);
|
||||
}, [t]);
|
||||
|
||||
useEffect(() => { if (!AuthService.getUser()) { router.push('/login'); return; } loadReservations(); }, [router, loadReservations]);
|
||||
|
||||
const loadPaymentMethods = useCallback(async () => {
|
||||
setLoadingPaymentMethods(true);
|
||||
try {
|
||||
const data = await getPaymentTypes();
|
||||
setPaymentMethods(Array.isArray(data) ? data : []);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
setPaymentMethods([]);
|
||||
} finally {
|
||||
setLoadingPaymentMethods(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
loadPaymentMethods();
|
||||
}, [loadPaymentMethods]);
|
||||
|
||||
useEffect(() => {
|
||||
let r = reservations;
|
||||
if (filterStatus !== 'all') r = r.filter(x => STATUS_MAP[x.status] === filterStatus);
|
||||
@ -1301,6 +1305,8 @@ export default function UserReservationsPage() {
|
||||
onSelectPayment={setSelectedPayment}
|
||||
onConfirmPay={handleConfirmPay}
|
||||
payingId={payingId}
|
||||
paymentMethods={paymentMethods}
|
||||
loadingPaymentMethods={loadingPaymentMethods}
|
||||
/>
|
||||
<div className="container mx-auto px-4">
|
||||
<motion.div initial={{opacity:0,y:-20}} animate={{opacity:1,y:0}} className="mb-8">
|
||||
|
||||
@ -355,6 +355,10 @@ export async function getCurrencies() {
|
||||
return apiFetch("/Currency/GetAll");
|
||||
}
|
||||
|
||||
export async function getPaymentTypes() {
|
||||
return apiFetch("/PaymentType/GetAll");
|
||||
}
|
||||
|
||||
// ─── Files ───
|
||||
|
||||
export async function uploadPicture(file) {
|
||||
|
||||
Reference in New Issue
Block a user