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 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 || {};
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user