Added payment types
All checks were successful
Build frontend / build (push) Successful in 1m56s

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 rawImages = Array.isArray(info.images) ? info.images : [];
const photosFallback = Array.isArray(details.photos) ? details.photos : []; const photosFallback = Array.isArray(details.photos) ? details.photos : [];
const allRaw = rawImages.length > 0 ? rawImages : photosFallback; 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 // Normalize services: Flutter sends list of strings or object with boolean values
const rawServices = details.services || {}; const rawServices = details.services || {};

View File

@ -425,7 +425,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 '../services/AuthService'; 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'; import { addPropertyRating } from '../utils/ratings';
const API_BASE = process.env.NEXT_PUBLIC_API_URL || 'https://45.93.137.91.nip.io/api'; 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 }, 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 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; } function statusIcon(code) { return STATUS_UI[STATUS_MAP[code]]?.icon ?? Clock; }
@ -667,6 +636,8 @@ function PaymentDialog({
onSelectPayment, onSelectPayment,
onConfirmPay, onConfirmPay,
payingId, payingId,
paymentMethods,
loadingPaymentMethods,
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const [showCashDialog, setShowCashDialog] = useState(false); const [showCashDialog, setShowCashDialog] = useState(false);
@ -745,52 +716,66 @@ function PaymentDialog({
<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('paymentMethod')}</p> <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"> {loadingPaymentMethods ? (
{PAYMENT_METHODS.map((method) => { <div className="rounded-2xl border border-dashed border-gray-200 bg-gray-50 p-4 text-sm text-gray-600">
const isSelected = selectedPayment === method.id; {t('loadingPaymentMethods', { defaultValue: 'جاري تحميل طرق الدفع...' })}
const Icon = method.icon; </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 ( 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'
: '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 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' isSelected ? 'bg-amber-500 text-white' : 'bg-gray-100 text-gray-500'
}`}> }`}>
<Icon className="w-5 h-5" /> {isActive ? <CreditCard className="w-5 h-5" /> : <X 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" />
</div> </div>
)} <div className="min-w-0 flex-1">
</button> <p className={`text-sm font-bold ${isSelected ? 'text-amber-900' : 'text-gray-800'}`}>
); {label}
})} </p>
</div> <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>
<div className="px-6 py-5 border-b border-gray-100"> <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 [reportingId, setReportingId] = useState(null);
const [selectedPayment, setSelectedPayment] = useState('cash'); const [selectedPayment, setSelectedPayment] = useState('cash');
const [paymentDialog, setPaymentDialog] = useState({ open: false, reservation: null }); const [paymentDialog, setPaymentDialog] = useState({ open: false, reservation: null });
const [paymentMethods, setPaymentMethods] = useState([]);
useEffect(() => { if (!AuthService.getUser()) { router.push('/login'); return; } loadReservations(); }, [router]); const [loadingPaymentMethods, setLoadingPaymentMethods] = useState(false);
const loadReservations = useCallback(async () => { const loadReservations = useCallback(async () => {
try { try {
@ -1207,8 +1192,27 @@ export default function UserReservationsPage() {
setFiltered([]); setFiltered([]);
} }
setLoading(false); 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(() => { useEffect(() => {
let r = reservations; let r = reservations;
if (filterStatus !== 'all') r = r.filter(x => STATUS_MAP[x.status] === filterStatus); if (filterStatus !== 'all') r = r.filter(x => STATUS_MAP[x.status] === filterStatus);
@ -1301,6 +1305,8 @@ export default function UserReservationsPage() {
onSelectPayment={setSelectedPayment} onSelectPayment={setSelectedPayment}
onConfirmPay={handleConfirmPay} onConfirmPay={handleConfirmPay}
payingId={payingId} payingId={payingId}
paymentMethods={paymentMethods}
loadingPaymentMethods={loadingPaymentMethods}
/> />
<div className="container mx-auto px-4"> <div className="container mx-auto px-4">
<motion.div initial={{opacity:0,y:-20}} animate={{opacity:1,y:0}} className="mb-8"> <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"); return apiFetch("/Currency/GetAll");
} }
export async function getPaymentTypes() {
return apiFetch("/PaymentType/GetAll");
}
// ─── Files ─── // ─── Files ───
export async function uploadPicture(file) { export async function uploadPicture(file) {