Added payment types

This commit is contained in:
Rahaf
2026-07-04 01:49:04 +03:00
parent c7698741c7
commit 3b66bb53f1
3 changed files with 89 additions and 79 deletions

View File

@ -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 || {};

View File

@ -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">

View File

@ -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) {