add payment for SaleProprite ,add commsion,fixed error handling fiexd final things
All checks were successful
Build frontend / build (push) Successful in 1m46s
All checks were successful
Build frontend / build (push) Successful in 1m46s
This commit is contained in:
@ -256,16 +256,7 @@ export default function ClientLayout({ children }) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* <motion.button
|
|
||||||
whileHover={{ scale: 1.1, rotate: 360 }}
|
|
||||||
whileTap={{ scale: 0.9 }}
|
|
||||||
onClick={() =>
|
|
||||||
changeLanguage(currentLanguage === "en" ? "ar" : "en")
|
|
||||||
}
|
|
||||||
className="flex items-center justify-center w-10 h-10 bg-gray-100 hover:bg-gray-200 rounded-full transition-all duration-200 ml-4"
|
|
||||||
>
|
|
||||||
<Globe className="w-5 h-5 text-gray-700" />
|
|
||||||
</motion.button> */}
|
|
||||||
|
|
||||||
{user && (
|
{user && (
|
||||||
<div className="relative" ref={menuRef}>
|
<div className="relative" ref={menuRef}>
|
||||||
|
|||||||
@ -383,7 +383,7 @@ export default function OwnerBookingsPage() {
|
|||||||
|
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
try {
|
try {
|
||||||
const [response, rentProps] = await Promise.all([getOwnerReservationsByStatuses([0, 1, 2, 3, 4]), getRentProperties().catch(() => [])]);
|
const [response, rentProps] = await Promise.all([getOwnerReservationsByStatuses([0, 1, 2, 3, 4, 5]), getRentProperties().catch(() => [])]);
|
||||||
|
|
||||||
let rawData = response;
|
let rawData = response;
|
||||||
|
|
||||||
@ -419,8 +419,6 @@ export default function OwnerBookingsPage() {
|
|||||||
propMap[rp.propertyInformation?.id] = propertyData;
|
propMap[rp.propertyInformation?.id] = propertyData;
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("PROP MAP:", propMap);
|
|
||||||
|
|
||||||
// ربط الـ reservation بالعقار عن طريق propertyId
|
// ربط الـ reservation بالعقار عن طريق propertyId
|
||||||
const enriched = list.map((item) => {
|
const enriched = list.map((item) => {
|
||||||
if (item.propertyId && propMap[item.propertyId]) {
|
if (item.propertyId && propMap[item.propertyId]) {
|
||||||
@ -430,8 +428,6 @@ export default function OwnerBookingsPage() {
|
|||||||
return item;
|
return item;
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("ENRICHED BOOKINGS:", enriched);
|
|
||||||
|
|
||||||
const mappedBookings = enriched.map((item) => {
|
const mappedBookings = enriched.map((item) => {
|
||||||
const property = item._prop || {};
|
const property = item._prop || {};
|
||||||
|
|
||||||
@ -450,7 +446,8 @@ export default function OwnerBookingsPage() {
|
|||||||
1: "pending",
|
1: "pending",
|
||||||
2: "confirmed",
|
2: "confirmed",
|
||||||
3: "completed",
|
3: "completed",
|
||||||
4: "cancelled",
|
4: "منتهية",
|
||||||
|
5: "cancelled",
|
||||||
};
|
};
|
||||||
|
|
||||||
const statusKey = statusMap[item?.status] || "pending";
|
const statusKey = statusMap[item?.status] || "pending";
|
||||||
|
|||||||
@ -1731,7 +1731,6 @@ export default function AddPropertyPage() {
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-bold text-gray-900 mb-4">{t("addProperty.imagesTitle")}</h3>
|
<h3 className="text-lg font-bold text-gray-900 mb-4">{t("addProperty.imagesTitle")}</h3>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
onClick={() => fileInputRef.current?.click()}
|
onClick={() => fileInputRef.current?.click()}
|
||||||
className={`border-2 border-dashed rounded-xl p-8 text-center cursor-pointer transition-all ${
|
className={`border-2 border-dashed rounded-xl p-8 text-center cursor-pointer transition-all ${
|
||||||
|
|||||||
@ -18,30 +18,16 @@ import {
|
|||||||
Trash2,
|
Trash2,
|
||||||
Eye,
|
Eye,
|
||||||
Calendar,
|
Calendar,
|
||||||
TrendingUp,
|
Layers,
|
||||||
Users,
|
Sofa,
|
||||||
FileText,
|
DoorOpen,
|
||||||
Image as ImageIcon,
|
ImageIcon,
|
||||||
CheckCircle,
|
CheckCircle,
|
||||||
XCircle,
|
XCircle,
|
||||||
AlertCircle,
|
AlertCircle,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
ChevronLeft,
|
ChevronLeft,
|
||||||
Loader2,
|
Loader2,
|
||||||
Clock,
|
|
||||||
Wifi,
|
|
||||||
Zap,
|
|
||||||
Flame,
|
|
||||||
Droplets,
|
|
||||||
Cigarette,
|
|
||||||
Dog,
|
|
||||||
Music,
|
|
||||||
Warehouse,
|
|
||||||
Layers,
|
|
||||||
Sofa,
|
|
||||||
DoorOpen,
|
|
||||||
Wind,
|
|
||||||
Pencil,
|
|
||||||
Save,
|
Save,
|
||||||
X,
|
X,
|
||||||
Star,
|
Star,
|
||||||
@ -52,10 +38,23 @@ import {
|
|||||||
Store,
|
Store,
|
||||||
GraduationCap,
|
GraduationCap,
|
||||||
TreePine,
|
TreePine,
|
||||||
|
CreditCard,
|
||||||
|
Banknote, // تم إضافة Banknote هنا
|
||||||
} 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 { buildRentPropertyPayload, getMyRentListings, getMySaleListings, editRentProperty, editSaleProperty, updateRentPropertyStatus, updateSalePropertyStatus, uploadPicture } from "../../utils/api";
|
import {
|
||||||
|
buildRentPropertyPayload,
|
||||||
|
getMyRentListings,
|
||||||
|
getMySaleListings,
|
||||||
|
editRentProperty,
|
||||||
|
editSaleProperty,
|
||||||
|
updateRentPropertyStatus,
|
||||||
|
updateSalePropertyStatus,
|
||||||
|
uploadPicture,
|
||||||
|
getPaymentTypes,
|
||||||
|
OwnerPaymentForSaleProperite, // تم إضافة استيراد الدالة هنا
|
||||||
|
} from "../../utils/api";
|
||||||
import { PropertyService } from "../../enums/PropertyService";
|
import { PropertyService } from "../../enums/PropertyService";
|
||||||
import { PropertyTerm } from "../../enums/PropertyTerm";
|
import { PropertyTerm } from "../../enums/PropertyTerm";
|
||||||
import Loading from "@/app/loading";
|
import Loading from "@/app/loading";
|
||||||
@ -97,6 +96,249 @@ const proximityI18nKeys = {
|
|||||||
BusStation: "proximity.busStation",
|
BusStation: "proximity.busStation",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const CommissionPaymentDialog = ({
|
||||||
|
isOpen,
|
||||||
|
property,
|
||||||
|
onClose,
|
||||||
|
selectedPayment,
|
||||||
|
onSelectPayment,
|
||||||
|
onConfirmPay,
|
||||||
|
payingId,
|
||||||
|
paymentMethods,
|
||||||
|
loadingPaymentMethods,
|
||||||
|
receiptImage,
|
||||||
|
onSetReceiptImage,
|
||||||
|
commissionDetails,
|
||||||
|
setCommissionDetails,
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
if (!isOpen || !property) return null;
|
||||||
|
|
||||||
|
const commission = Number(property.commission || 0);
|
||||||
|
|
||||||
|
const selectedMethod = paymentMethods?.find((m) => String(m?.id ?? m?.name) === String(selectedPayment));
|
||||||
|
|
||||||
|
const isHaram = selectedMethod?.name?.toLowerCase?.() === "haram";
|
||||||
|
|
||||||
|
const hasSelectedMethod = !!selectedMethod;
|
||||||
|
|
||||||
|
const hasHaramDetails = commissionDetails?.fullName?.trim() && commissionDetails?.governorate?.trim() && commissionDetails?.phoneNumber?.trim();
|
||||||
|
|
||||||
|
const canPay = payingId !== property.id && hasSelectedMethod && (!isHaram || (!!receiptImage && !!hasHaramDetails));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 px-4 py-8" onClick={onClose}>
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, scale: 0.95 }}
|
||||||
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
|
className="w-full max-w-2xl rounded-3xl bg-white shadow-2xl border border-gray-200 overflow-hidden"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
{/* Header */}
|
||||||
|
<div className="p-6 border-b border-gray-100">
|
||||||
|
<div className="flex items-start gap-4">
|
||||||
|
<div className="w-14 h-14 bg-amber-100 rounded-2xl flex items-center justify-center">
|
||||||
|
<DollarSign className="w-7 h-7 text-amber-600" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1">
|
||||||
|
<h3 className="text-xl font-bold text-gray-900">{property.title}</h3>
|
||||||
|
|
||||||
|
<p className="text-sm text-gray-500 mt-1">{t("commission")}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Commission amount */}
|
||||||
|
<div className="px-6 py-5 border-b border-gray-100">
|
||||||
|
<div className="text-center">
|
||||||
|
<p className="text-sm text-gray-500 mb-1">{t("commission")}</p>
|
||||||
|
|
||||||
|
<p className="text-4xl font-bold text-amber-600">
|
||||||
|
{commission.toLocaleString()} {t("syp")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Payment methods */}
|
||||||
|
<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>
|
||||||
|
|
||||||
|
{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={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 opacity-50 cursor-not-allowed"
|
||||||
|
: isSelected
|
||||||
|
? "border-amber-500 bg-amber-600 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"}`}>
|
||||||
|
<CreditCard className="w-5 h-5" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<p className="text-sm font-bold text-gray-800">{label}</p>
|
||||||
|
|
||||||
|
<p className="text-xs text-gray-800 mt-0.5">
|
||||||
|
{isActive
|
||||||
|
? t("paymentMethodAvailable", {
|
||||||
|
defaultValue: "متاحة للاستعمال",
|
||||||
|
})
|
||||||
|
: t("paymentMethodUnavailable", {
|
||||||
|
defaultValue: "غير متاحة حالياً",
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
</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>
|
||||||
|
{/* Haram payment details */}
|
||||||
|
{isHaram && (
|
||||||
|
<div className="px-6 py-5 border-b border-gray-100">
|
||||||
|
<p className="text-sm font-bold text-gray-700 mb-3">بيانات الدفع</p>
|
||||||
|
|
||||||
|
<div className="space-y-2 text-[20px]">
|
||||||
|
{commissionDetails.split("\n").map((line, idx) => (
|
||||||
|
<div key={idx}>{line}</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{/* Receipt */}
|
||||||
|
{isHaram && (
|
||||||
|
<div className="px-6 py-5 border-b border-gray-100">
|
||||||
|
<p className="text-sm font-bold text-gray-700 mb-3">
|
||||||
|
{t("uploadReceipt", {
|
||||||
|
defaultValue: "إيصال الدفع",
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{receiptImage ? (
|
||||||
|
<div className="relative rounded-2xl overflow-hidden border-2 border-amber-200 bg-amber-50">
|
||||||
|
<img src={URL.createObjectURL(receiptImage)} alt="receipt" className="w-full h-48 object-contain" />
|
||||||
|
|
||||||
|
<button type="button" onClick={() => onSetReceiptImage(null)} className="absolute top-2 left-2 w-8 h-8 bg-red-500 text-white rounded-full flex items-center justify-center">
|
||||||
|
<X className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="p-3 text-center">
|
||||||
|
<p className="text-xs text-gray-500 truncate">{receiptImage.name}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<label className="flex flex-col items-center justify-center w-full h-48 border-2 border-dashed border-gray-300 rounded-2xl bg-gray-50 cursor-pointer">
|
||||||
|
<ImageIcon className="w-10 h-10 text-gray-400 mb-2" />
|
||||||
|
|
||||||
|
<p className="text-sm text-gray-500">
|
||||||
|
{t("tapToUploadReceipt", {
|
||||||
|
defaultValue: "اضغط لرفع صورة الإيصال",
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept="image/jpeg,image/png,image/jpg"
|
||||||
|
className="hidden"
|
||||||
|
onChange={(e) => {
|
||||||
|
const file = e.target.files?.[0];
|
||||||
|
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
if (file.size > 5 * 1024 * 1024) {
|
||||||
|
toast.error(
|
||||||
|
t("fileTooLarge", {
|
||||||
|
defaultValue: "الصورة كبيرة جداً، الحد الأقصى 5 ميجا",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
onSetReceiptImage(file);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Pay */}
|
||||||
|
<div className="px-6 py-5">
|
||||||
|
<motion.button
|
||||||
|
type="button"
|
||||||
|
onClick={() => onConfirmPay(property)}
|
||||||
|
whileHover={{ scale: 1.01 }}
|
||||||
|
whileTap={{ scale: 0.99 }}
|
||||||
|
className="w-full bg-amber-500 hover:bg-amber-600 disabled:bg-amber-300 text-white font-bold py-4 px-6 rounded-2xl text-lg transition-all flex items-center justify-center gap-3"
|
||||||
|
>
|
||||||
|
{payingId === property.id ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="w-5 h-5 animate-spin" />
|
||||||
|
{t("processingPayment")}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Banknote className="w-5 h-5" />
|
||||||
|
{t("payCommission", {
|
||||||
|
defaultValue: "دفع الكمسيون",
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</motion.button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-4 flex justify-end">
|
||||||
|
<button type="button" onClick={onClose} className="px-5 py-2.5 rounded-xl border border-gray-300 text-gray-700 hover:bg-gray-100">
|
||||||
|
{t("close")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const DeleteConfirmationModal = ({ isOpen, onClose, onConfirm, propertyTitle }) => {
|
const DeleteConfirmationModal = ({ isOpen, onClose, onConfirm, propertyTitle }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
if (!isOpen) return null;
|
if (!isOpen) return null;
|
||||||
@ -503,9 +745,11 @@ const PropertyEditModal = ({ isOpen, onClose, property, onSave }) => {
|
|||||||
nearbyUniversity: "",
|
nearbyUniversity: "",
|
||||||
nearbyPark: "",
|
nearbyPark: "",
|
||||||
nearbyMall: "",
|
nearbyMall: "",
|
||||||
|
nearbyMosque: "",
|
||||||
purpose: "rent",
|
purpose: "rent",
|
||||||
currencyId: 1,
|
currencyId: 1,
|
||||||
dailyPrice: 0,
|
dailyPrice: 0,
|
||||||
|
commission: "",
|
||||||
monthlyPrice: 0,
|
monthlyPrice: 0,
|
||||||
deposit: 0,
|
deposit: 0,
|
||||||
rentType: "monthly",
|
rentType: "monthly",
|
||||||
@ -591,6 +835,7 @@ const PropertyEditModal = ({ isOpen, onClose, property, onSave }) => {
|
|||||||
serviceDetails,
|
serviceDetails,
|
||||||
terms,
|
terms,
|
||||||
customTerms: details.customTerms || [],
|
customTerms: details.customTerms || [],
|
||||||
|
nearbyMosque: prox.nearbyMosque || prox.neaebymosque || "",
|
||||||
nearbySchool: prox.School ?? prox.school ?? prox.nearbySchool ?? "",
|
nearbySchool: prox.School ?? prox.school ?? prox.nearbySchool ?? "",
|
||||||
nearbyHospital: prox.Hospital ?? prox.hospital ?? prox.nearbyHospital ?? "",
|
nearbyHospital: prox.Hospital ?? prox.hospital ?? prox.nearbyHospital ?? "",
|
||||||
nearbyRestaurant: prox.Restaurant ?? prox.restaurant ?? prox.nearbyRestaurant ?? "",
|
nearbyRestaurant: prox.Restaurant ?? prox.restaurant ?? prox.nearbyRestaurant ?? "",
|
||||||
@ -599,6 +844,7 @@ const PropertyEditModal = ({ isOpen, onClose, property, onSave }) => {
|
|||||||
nearbyMall: prox.Mall ?? prox.mall ?? prox.nearbyMall ?? "",
|
nearbyMall: prox.Mall ?? prox.mall ?? prox.nearbyMall ?? "",
|
||||||
purpose: property.purpose || "rent",
|
purpose: property.purpose || "rent",
|
||||||
currencyId: property.currencyId || 1,
|
currencyId: property.currencyId || 1,
|
||||||
|
commission: property.commission || prox.commission || "",
|
||||||
...(property.purpose === "rent"
|
...(property.purpose === "rent"
|
||||||
? {
|
? {
|
||||||
dailyPrice: property.dailyPrice || 0,
|
dailyPrice: property.dailyPrice || 0,
|
||||||
@ -791,7 +1037,7 @@ const PropertyEditModal = ({ isOpen, onClose, property, onSave }) => {
|
|||||||
<select
|
<select
|
||||||
value={formData.propertyType}
|
value={formData.propertyType}
|
||||||
onChange={(e) => handleChange("propertyType", e.target.value)}
|
onChange={(e) => handleChange("propertyType", e.target.value)}
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-amber-500"
|
className="w-full px-3 bg-[#0f172a] text-white py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-amber-500"
|
||||||
>
|
>
|
||||||
<option value="apartment">{t("buildingType.apartment")}</option>
|
<option value="apartment">{t("buildingType.apartment")}</option>
|
||||||
<option value="villa">{t("buildingType.villa")}</option>
|
<option value="villa">{t("buildingType.villa")}</option>
|
||||||
@ -1025,6 +1271,7 @@ const PropertyEditModal = ({ isOpen, onClose, property, onSave }) => {
|
|||||||
{ id: "nearbyUniversity", label: t("university") },
|
{ id: "nearbyUniversity", label: t("university") },
|
||||||
{ id: "nearbyPark", label: t("park") },
|
{ id: "nearbyPark", label: t("park") },
|
||||||
{ id: "nearbyMall", label: t("mall") },
|
{ id: "nearbyMall", label: t("mall") },
|
||||||
|
{ id: "nearbyMosque", label: t("neaebymosque") },
|
||||||
].map((field) => (
|
].map((field) => (
|
||||||
<div key={field.id}>
|
<div key={field.id}>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">{field.label}</label>
|
<label className="block text-sm font-medium text-gray-700 mb-1">{field.label}</label>
|
||||||
@ -1077,12 +1324,23 @@ const PropertyEditModal = ({ isOpen, onClose, property, onSave }) => {
|
|||||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-amber-500"
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-amber-500"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">{t("commission")}</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
value={formData.commission}
|
||||||
|
onChange={(e) => handleChange("commission", Number(e.target.value))}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-amber-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">{t("rentType")}</label>
|
<label className="block text-sm font-medium text-gray-700 mb-1">{t("rentType")}</label>
|
||||||
<select
|
<select
|
||||||
value={formData.rentType}
|
value={formData.rentType}
|
||||||
onChange={(e) => handleChange("rentType", e.target.value)}
|
onChange={(e) => handleChange("rentType", e.target.value)}
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-amber-500"
|
className="w-full px-3 bg-[#0f172a] text-white py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-amber-500"
|
||||||
>
|
>
|
||||||
<option value="daily">{t("daily")}</option>
|
<option value="daily">{t("daily")}</option>
|
||||||
<option value="monthly">{t("monthly")}</option>
|
<option value="monthly">{t("monthly")}</option>
|
||||||
@ -1094,7 +1352,7 @@ const PropertyEditModal = ({ isOpen, onClose, property, onSave }) => {
|
|||||||
<select
|
<select
|
||||||
value={formData.allowedPaymentPeriod}
|
value={formData.allowedPaymentPeriod}
|
||||||
onChange={(e) => handleChange("allowedPaymentPeriod", e.target.value)}
|
onChange={(e) => handleChange("allowedPaymentPeriod", e.target.value)}
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-amber-500"
|
className="w-full px-3 py-2 border bg-[#0f172a] text-white border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-amber-500"
|
||||||
>
|
>
|
||||||
<option value="">{t("selectPeriod")}</option>
|
<option value="">{t("selectPeriod")}</option>
|
||||||
<option value="1.00:00:00">{t("daily")}</option>
|
<option value="1.00:00:00">{t("daily")}</option>
|
||||||
@ -1155,7 +1413,17 @@ export default function OwnerPropertiesPage() {
|
|||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [showAddMenu, setShowAddMenu] = useState(false);
|
const [showAddMenu, setShowAddMenu] = useState(false);
|
||||||
const [activeTab, setActiveTab] = useState("rent");
|
const [activeTab, setActiveTab] = useState("rent");
|
||||||
|
const [commissionPayment, setCommissionPayment] = useState({
|
||||||
|
open: false,
|
||||||
|
property: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [selectedCommissionPayment, setSelectedCommissionPayment] = useState(null);
|
||||||
|
const [commissionPaymentMethods, setCommissionPaymentMethods] = useState([]);
|
||||||
|
const [loadingCommissionPaymentMethods, setLoadingCommissionPaymentMethods] = useState(false);
|
||||||
|
const [commissionReceiptImage, setCommissionReceiptImage] = useState(null);
|
||||||
|
const [commissionPayingId, setCommissionPayingId] = useState(null);
|
||||||
|
const [commissionDetails, setCommissionDetails] = useState("");
|
||||||
const [deleteModal, setDeleteModal] = useState({
|
const [deleteModal, setDeleteModal] = useState({
|
||||||
isOpen: false,
|
isOpen: false,
|
||||||
property: null,
|
property: null,
|
||||||
@ -1186,6 +1454,29 @@ export default function OwnerPropertiesPage() {
|
|||||||
}
|
}
|
||||||
}, [router]);
|
}, [router]);
|
||||||
|
|
||||||
|
const openCommissionPayment = async (property) => {
|
||||||
|
setCommissionPayment({
|
||||||
|
open: true,
|
||||||
|
property,
|
||||||
|
});
|
||||||
|
|
||||||
|
setSelectedCommissionPayment(null);
|
||||||
|
setCommissionReceiptImage(null);
|
||||||
|
setLoadingCommissionPaymentMethods(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await getPaymentTypes();
|
||||||
|
|
||||||
|
setCommissionDetails(result[0].details);
|
||||||
|
|
||||||
|
setCommissionPaymentMethods(result?.value || result?.data || result || []);
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(error?.message || "فشل تحميل طرق الدفع");
|
||||||
|
} finally {
|
||||||
|
setLoadingCommissionPaymentMethods(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const loadProperties = async () => {
|
const loadProperties = async () => {
|
||||||
const authUser = AuthService.getUser();
|
const authUser = AuthService.getUser();
|
||||||
const userId = authUser?.id;
|
const userId = authUser?.id;
|
||||||
@ -1275,6 +1566,7 @@ export default function OwnerPropertiesPage() {
|
|||||||
monthlyPrice: item.monthlyRent || 0,
|
monthlyPrice: item.monthlyRent || 0,
|
||||||
salePrice: item.price || 0,
|
salePrice: item.price || 0,
|
||||||
deposit: item.deposit || 0,
|
deposit: item.deposit || 0,
|
||||||
|
commission: item.commission || details.commission || 0,
|
||||||
location: info.address || "",
|
location: info.address || "",
|
||||||
bedrooms: info.numberOfBedRooms || 0,
|
bedrooms: info.numberOfBedRooms || 0,
|
||||||
bathrooms: info.numberOfBathRooms || 0,
|
bathrooms: info.numberOfBathRooms || 0,
|
||||||
@ -1299,6 +1591,7 @@ export default function OwnerPropertiesPage() {
|
|||||||
rating: item.rating || 0,
|
rating: item.rating || 0,
|
||||||
currencyId: item.currencyId,
|
currencyId: item.currencyId,
|
||||||
_raw: item,
|
_raw: item,
|
||||||
|
status2: item.status,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -1314,6 +1607,7 @@ export default function OwnerPropertiesPage() {
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
const apiBase = typeof window !== "undefined" ? process.env.NEXT_PUBLIC_API_URL || "https://45.93.137.91.nip.io/api" : "";
|
const apiBase = typeof window !== "undefined" ? process.env.NEXT_PUBLIC_API_URL || "https://45.93.137.91.nip.io/api" : "";
|
||||||
const raw = Array.isArray(info.images) ? info.images : [];
|
const raw = Array.isArray(info.images) ? info.images : [];
|
||||||
return {
|
return {
|
||||||
@ -1348,6 +1642,7 @@ export default function OwnerPropertiesPage() {
|
|||||||
monthlyPrice: 0,
|
monthlyPrice: 0,
|
||||||
salePrice: item.price || 0,
|
salePrice: item.price || 0,
|
||||||
deposit: 0,
|
deposit: 0,
|
||||||
|
commission: item.commission || details.commission || 0,
|
||||||
location: info.address || "",
|
location: info.address || "",
|
||||||
bedrooms: info.numberOfBedRooms || 0,
|
bedrooms: info.numberOfBedRooms || 0,
|
||||||
bathrooms: info.numberOfBathRooms || 0,
|
bathrooms: info.numberOfBathRooms || 0,
|
||||||
@ -1372,6 +1667,7 @@ export default function OwnerPropertiesPage() {
|
|||||||
rating: item.rating || 0,
|
rating: item.rating || 0,
|
||||||
currencyId: item.currencyId,
|
currencyId: item.currencyId,
|
||||||
_raw: item,
|
_raw: item,
|
||||||
|
status2: item.status,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -1531,6 +1827,7 @@ export default function OwnerPropertiesPage() {
|
|||||||
try {
|
try {
|
||||||
if (prop.purpose === "rent") {
|
if (prop.purpose === "rent") {
|
||||||
await updateRentPropertyStatus(prop.id, statusCode);
|
await updateRentPropertyStatus(prop.id, statusCode);
|
||||||
|
await updateRentPropertyStatus(prop.id, statusCode);
|
||||||
} else {
|
} else {
|
||||||
await updateSalePropertyStatus(prop.id, statusCode);
|
await updateSalePropertyStatus(prop.id, statusCode);
|
||||||
}
|
}
|
||||||
@ -1551,10 +1848,47 @@ export default function OwnerPropertiesPage() {
|
|||||||
transition: { duration: 0.5 },
|
transition: { duration: 0.5 },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleCommissionPayment = async (property) => {
|
||||||
|
if (!property) return;
|
||||||
|
|
||||||
|
if (!selectedCommissionPayment) {
|
||||||
|
toast.error("اختر طريقة الدفع");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedMethod = commissionPaymentMethods.find((m) => String(m?.id ?? m?.name) === String(selectedCommissionPayment));
|
||||||
|
|
||||||
|
const isHaram = selectedMethod?.name?.toLowerCase?.() === "haram";
|
||||||
|
|
||||||
|
if (isHaram && !commissionReceiptImage) {
|
||||||
|
toast.error("يرجى رفع إيصال الدفع");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setCommissionPayingId(property.id);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await OwnerPaymentForSaleProperite(property.id, selectedCommissionPayment, 1, commissionReceiptImage);
|
||||||
|
|
||||||
|
toast.success("تم إرسال طلب دفع الكمسيون");
|
||||||
|
await loadProperties();
|
||||||
|
setCommissionPayment({
|
||||||
|
open: false,
|
||||||
|
property: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
setCommissionReceiptImage(null);
|
||||||
|
setSelectedCommissionPayment(null);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("COMMISSION PAYMENT ERROR:", error);
|
||||||
|
toast.error(error?.message || "فشل دفع الكمسيون");
|
||||||
|
} finally {
|
||||||
|
setCommissionPayingId(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return <Loading />;
|
||||||
<Loading/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -1604,7 +1938,7 @@ export default function OwnerPropertiesPage() {
|
|||||||
<div className="p-2">
|
<div className="p-2">
|
||||||
<Link
|
<Link
|
||||||
href="/owner/properties/add?purpose=rent"
|
href="/owner/properties/add?purpose=rent"
|
||||||
className="flex items-center gap-3 px-4 py-3 text-gray-700 hover:bg-[#0f172a] rounded-lg transition-colors"
|
className="flex items-center gap-3 px-4 py-3 text-gray-700 hover:bg-[#0f172a] hover:text-white rounded-lg transition-colors"
|
||||||
onClick={() => setShowAddMenu(false)}
|
onClick={() => setShowAddMenu(false)}
|
||||||
>
|
>
|
||||||
<div className="w-8 h-8 bg-green-100 rounded-lg flex items-center justify-center">
|
<div className="w-8 h-8 bg-green-100 rounded-lg flex items-center justify-center">
|
||||||
@ -1618,7 +1952,7 @@ export default function OwnerPropertiesPage() {
|
|||||||
|
|
||||||
<Link
|
<Link
|
||||||
href="/owner/properties/add?purpose=sale"
|
href="/owner/properties/add?purpose=sale"
|
||||||
className="flex items-center gap-3 px-4 py-3 text-gray-700 hover:bg-[#0f172a] rounded-lg transition-colors"
|
className="flex items-center gap-3 px-4 py-3 text-gray-700 hover:bg-[#0f172a] hover:text-white rounded-lg transition-colors"
|
||||||
onClick={() => setShowAddMenu(false)}
|
onClick={() => setShowAddMenu(false)}
|
||||||
>
|
>
|
||||||
<div className="w-8 h-8 bg-blue-100 rounded-lg flex items-center justify-center">
|
<div className="w-8 h-8 bg-blue-100 rounded-lg flex items-center justify-center">
|
||||||
@ -1684,196 +2018,233 @@ export default function OwnerPropertiesPage() {
|
|||||||
initial="initial"
|
initial="initial"
|
||||||
animate="animate"
|
animate="animate"
|
||||||
transition={{ delay: index * 0.1 }}
|
transition={{ delay: index * 0.1 }}
|
||||||
className="bg-white rounded-2xl shadow-sm hover:shadow-md transition-all overflow-hidden border border-gray-200"
|
className="bg-white rounded-2xl shadow-sm hover:shadow-md transition-all overflow-hidden border border-gray-200 flex flex-col justify-between"
|
||||||
>
|
>
|
||||||
<div className="relative aspect-[16/10] bg-gray-100 overflow-hidden">
|
<div>
|
||||||
{property.images && property.images.length > 0 ? (
|
<div className="relative aspect-[16/10] bg-gray-100 overflow-hidden">
|
||||||
<img src={property.images[0]} alt={property.title} className="w-full h-full object-cover" />
|
{property.images && property.images.length > 0 ? (
|
||||||
) : (
|
<img src={property.images[0]} alt={property.title} className="w-full h-full object-cover" />
|
||||||
<div className="w-full h-full flex items-center justify-center">
|
) : (
|
||||||
<ImageIcon className="w-12 h-12 text-gray-400" />
|
<div className="w-full h-full flex items-center justify-center">
|
||||||
</div>
|
<ImageIcon className="w-12 h-12 text-gray-400" />
|
||||||
)}
|
|
||||||
<div className="absolute top-2 right-2 flex gap-1">
|
|
||||||
<span
|
|
||||||
className={`px-2 py-0.5 rounded-md text-xs font-medium shadow-sm backdrop-blur-sm ${
|
|
||||||
property.status === "available" ? "bg-white/90 text-green-700" : property.status === "notAvailable" ? "bg-red-50/90 text-red-700" : "bg-white/90 text-yellow-700"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{property.status === "notAvailable" ? t("notAvailable") : property.status === "available" ? t("available") : t("postponed")}
|
|
||||||
</span>
|
|
||||||
{property.purpose === "rent" && property.furnished !== undefined && (
|
|
||||||
<span className={`px-2 py-0.5 rounded-md text-xs font-medium shadow-sm backdrop-blur-sm ${property.furnished ? "bg-white/90 text-amber-700" : "bg-white/90 text-gray-600"}`}>
|
|
||||||
{property.furnished ? t("furnished") : t("unfurnished")}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="absolute top-2 left-2">
|
|
||||||
<span className="px-2 py-0.5 rounded-md text-xs font-medium bg-white/90 text-amber-700 shadow-sm backdrop-blur-sm">
|
|
||||||
{property.propertyTypeLabel ||
|
|
||||||
(property.propertyType === "apartment"
|
|
||||||
? t("buildingType.apartment")
|
|
||||||
: property.propertyType === "villa"
|
|
||||||
? t("buildingType.villa")
|
|
||||||
: property.propertyType === "sweet"
|
|
||||||
? t("buildingType.sweet")
|
|
||||||
: property.propertyType === "room"
|
|
||||||
? t("buildingType.room")
|
|
||||||
: property.propertyType === "studio"
|
|
||||||
? t("buildingType.studio")
|
|
||||||
: property.propertyType === "office"
|
|
||||||
? t("buildingType.office")
|
|
||||||
: property.propertyType === "farms"
|
|
||||||
? t("buildingType.farms")
|
|
||||||
: property.propertyType === "shop"
|
|
||||||
? t("buildingType.shop")
|
|
||||||
: property.propertyType === "warehouse"
|
|
||||||
? t("buildingType.warehouse")
|
|
||||||
: t("offer"))}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="p-3">
|
|
||||||
<div className="flex items-start justify-between mb-1">
|
|
||||||
<h3 className="font-bold text-gray-900 truncate flex-1 ml-2">{property.title}</h3>
|
|
||||||
{property.rating > 0 && (
|
|
||||||
<div className="flex items-center gap-0.5 text-amber-600 shrink-0">
|
|
||||||
<Star className="w-3 h-3 fill-amber-500" />
|
|
||||||
<span className="text-xs font-medium">{Number(property.rating).toFixed(1)}</span>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
<div className="absolute top-2 right-2 flex gap-1">
|
||||||
|
<span
|
||||||
<div className="flex items-center gap-1 text-gray-500 text-xs mb-2">
|
className={`px-2 py-0.5 rounded-md text-xs font-medium shadow-sm backdrop-blur-sm ${
|
||||||
<MapPin className="w-3 h-3" />
|
property.status === "available" ? "bg-white/90 text-green-700" : property.status === "notAvailable" ? "bg-red-50/90 text-red-700" : "bg-white/90 text-yellow-700"
|
||||||
<span className="truncate">{property.address || property.location || t("locationUnknown")}</span>
|
}`}
|
||||||
</div>
|
|
||||||
|
|
||||||
{property.description && <p className="text-xs text-gray-600 leading-relaxed mb-2 line-clamp-2">{property.description}</p>}
|
|
||||||
|
|
||||||
<div className="flex flex-wrap items-center gap-2 text-xs text-gray-600 mb-2">
|
|
||||||
{property.bedrooms > 0 && (
|
|
||||||
<div className="flex items-center gap-0.5 bg-gray-50 px-2 py-0.5 rounded-md">
|
|
||||||
<Bed className="w-3 h-3" />
|
|
||||||
<span>{property.bedrooms}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{property.bathrooms > 0 && (
|
|
||||||
<div className="flex items-center gap-0.5 bg-gray-50 px-2 py-0.5 rounded-md">
|
|
||||||
<Bath className="w-3 h-3" />
|
|
||||||
<span>{property.bathrooms}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{property.area > 0 && (
|
|
||||||
<div className="flex items-center gap-0.5 bg-gray-50 px-2 py-0.5 rounded-md">
|
|
||||||
<Square className="w-3 h-3" />
|
|
||||||
<span>
|
|
||||||
{property.area}
|
|
||||||
{t("sqm")}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{property.floor > 0 && (
|
|
||||||
<div className="flex items-center gap-0.5 bg-gray-50 px-2 py-0.5 rounded-md">
|
|
||||||
<Layers className="w-3 h-3" />
|
|
||||||
<span>
|
|
||||||
{t("floor")} {property.floor}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{property.salons > 0 && (
|
|
||||||
<div className="flex items-center gap-0.5 bg-gray-50 px-2 py-0.5 rounded-md">
|
|
||||||
<Sofa className="w-3 h-3" />
|
|
||||||
<span>{property.salons}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{property.balconies > 0 && (
|
|
||||||
<div className="flex items-center gap-0.5 bg-gray-50 px-2 py-0.5 rounded-md">
|
|
||||||
<DoorOpen className="w-3 h-3" />
|
|
||||||
<span>{property.balconies}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex justify-between items-center pt-2 border-t border-gray-100">
|
|
||||||
<div>
|
|
||||||
{property.purpose === "rent" ? (
|
|
||||||
<div>
|
|
||||||
{property.monthlyPrice > 0 && (
|
|
||||||
<span className="text-base font-bold text-amber-600">
|
|
||||||
{Number(property.monthlyPrice).toLocaleString()}
|
|
||||||
<span className="text-xs text-gray-500 font-normal mr-1">{t("perMonth")}</span>
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{property.dailyPrice > 0 && !property.monthlyPrice && (
|
|
||||||
<span className="text-base font-bold text-amber-600">
|
|
||||||
{Number(property.dailyPrice).toLocaleString()}
|
|
||||||
<span className="text-xs text-gray-500 font-normal mr-1">{t("perDay")}</span>
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{property.deposit > 0 && <div className="text-[10px] text-gray-400">{t("depositAmount", { amount: Number(property.deposit).toLocaleString() })}</div>}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<span className="text-base font-bold text-blue-600">
|
|
||||||
{Number(property.salePrice).toLocaleString()} {t("syp")}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex gap-1">
|
|
||||||
{property.status === "notAvailable" ? (
|
|
||||||
<button
|
|
||||||
onClick={() =>
|
|
||||||
setDeactivateModal({
|
|
||||||
isOpen: true,
|
|
||||||
property,
|
|
||||||
isActivating: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
className="p-1.5 hover:bg-green-50 rounded-lg transition-colors group"
|
|
||||||
title={t("activateProperty")}
|
|
||||||
>
|
|
||||||
<CheckCircle className="w-3.5 h-3.5 text-gray-400 group-hover:text-green-600" />
|
|
||||||
</button>
|
|
||||||
) : (
|
|
||||||
<button
|
|
||||||
onClick={() =>
|
|
||||||
setDeactivateModal({
|
|
||||||
isOpen: true,
|
|
||||||
property,
|
|
||||||
isActivating: false,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
className="p-1.5 hover:bg-red-50 rounded-lg transition-colors group"
|
|
||||||
title={t("deactivateProperty")}
|
|
||||||
>
|
|
||||||
<XCircle className="w-3.5 h-3.5 text-gray-400 group-hover:text-red-600" />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
<button
|
|
||||||
onClick={() => setViewModal({ isOpen: true, property })}
|
|
||||||
className="px-3 py-1.5 bg-gray-50 hover:bg-blue-50 border border-gray-200 hover:border-blue-200 rounded-lg transition-all group flex items-center gap-1.5"
|
|
||||||
title={t("viewDetails")}
|
|
||||||
>
|
>
|
||||||
<Eye className="w-3.5 h-3.5 text-gray-400 group-hover:text-blue-600" />
|
{property.status === "notAvailable" ? t("notAvailable") : property.status === "available" ? t("available") : t("postponed")}
|
||||||
<span className="text-xs text-gray-500 group-hover:text-blue-600 font-medium">{t("view")}</span>
|
</span>
|
||||||
</button>
|
{property.purpose === "rent" && property.furnished !== undefined && (
|
||||||
<button onClick={() => setEditModal({ isOpen: true, property })} className="p-1.5 hover:bg-amber-50 rounded-lg transition-colors group" title={t("editProperty")}>
|
<span className={`px-2 py-0.5 rounded-md text-xs font-medium shadow-sm backdrop-blur-sm ${property.furnished ? "bg-white/90 text-amber-700" : "bg-white/90 text-gray-600"}`}>
|
||||||
<Edit className="w-3.5 h-3.5 text-gray-400 group-hover:text-amber-600" />
|
{property.furnished ? t("furnished") : t("unfurnished")}
|
||||||
</button>
|
</span>
|
||||||
<button onClick={() => setDeleteModal({ isOpen: true, property })} className="p-1.5 hover:bg-red-50 rounded-lg transition-colors group" title={t("deleteProperty")}>
|
)}
|
||||||
<Trash2 className="w-3.5 h-3.5 text-gray-400 group-hover:text-red-600" />
|
</div>
|
||||||
</button>
|
|
||||||
|
<div className="absolute top-2 left-2">
|
||||||
|
<span className="px-2 py-0.5 rounded-md text-xs font-medium bg-white/90 text-amber-700 shadow-sm backdrop-blur-sm">
|
||||||
|
{property.propertyTypeLabel ||
|
||||||
|
(property.propertyType === "apartment"
|
||||||
|
? t("buildingType.apartment")
|
||||||
|
: property.propertyType === "villa"
|
||||||
|
? t("buildingType.villa")
|
||||||
|
: property.propertyType === "sweet"
|
||||||
|
? t("buildingType.sweet")
|
||||||
|
: property.propertyType === "room"
|
||||||
|
? t("buildingType.room")
|
||||||
|
: property.propertyType === "studio"
|
||||||
|
? t("buildingType.studio")
|
||||||
|
: property.propertyType === "office"
|
||||||
|
? t("buildingType.office")
|
||||||
|
: property.propertyType === "farms"
|
||||||
|
? t("buildingType.farms")
|
||||||
|
: property.propertyType === "shop"
|
||||||
|
? t("buildingType.shop")
|
||||||
|
: property.propertyType === "warehouse"
|
||||||
|
? t("buildingType.warehouse")
|
||||||
|
: t("offer"))}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-3">
|
||||||
|
<div className="flex items-start justify-between mb-1">
|
||||||
|
<h3 className="font-bold text-gray-900 truncate flex-1 ml-2">{property.title}</h3>
|
||||||
|
{property.rating > 0 && (
|
||||||
|
<div className="flex items-center gap-0.5 text-amber-600 shrink-0">
|
||||||
|
<Star className="w-3 h-3 fill-amber-500" />
|
||||||
|
<span className="text-xs font-medium">{Number(property.rating).toFixed(1)}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-1 text-gray-500 text-xs mb-2">
|
||||||
|
<MapPin className="w-3 h-3" />
|
||||||
|
<span className="truncate">{property.address || property.location || t("locationUnknown")}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{property.description && <p className="text-xs text-gray-600 leading-relaxed mb-2 line-clamp-2">{property.description}</p>}
|
||||||
|
|
||||||
|
<div className="flex flex-wrap items-center gap-2 text-xs text-gray-600 mb-2">
|
||||||
|
{property.bedrooms > 0 && (
|
||||||
|
<div className="flex items-center gap-0.5 bg-gray-50 px-2 py-0.5 rounded-md">
|
||||||
|
<Bed className="w-3 h-3" />
|
||||||
|
<span>{property.bedrooms}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{property.bathrooms > 0 && (
|
||||||
|
<div className="flex items-center gap-0.5 bg-gray-50 px-2 py-0.5 rounded-md">
|
||||||
|
<Bath className="w-3 h-3" />
|
||||||
|
<span>{property.bathrooms}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{property.area > 0 && (
|
||||||
|
<div className="flex items-center gap-0.5 bg-gray-50 px-2 py-0.5 rounded-md">
|
||||||
|
<Square className="w-3 h-3" />
|
||||||
|
<span>
|
||||||
|
{property.area}
|
||||||
|
{t("sqm")}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{property.floor > 0 && (
|
||||||
|
<div className="flex items-center gap-0.5 bg-gray-50 px-2 py-0.5 rounded-md">
|
||||||
|
<Layers className="w-3 h-3" />
|
||||||
|
<span>
|
||||||
|
{t("floor")} {property.floor}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{property.salons > 0 && (
|
||||||
|
<div className="flex items-center gap-0.5 bg-gray-50 px-2 py-0.5 rounded-md">
|
||||||
|
<Sofa className="w-3 h-3" />
|
||||||
|
<span>{property.salons}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{property.balconies > 0 && (
|
||||||
|
<div className="flex items-center gap-0.5 bg-gray-50 px-2 py-0.5 rounded-md">
|
||||||
|
<DoorOpen className="w-3 h-3" />
|
||||||
|
<span>{property.balconies}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-between items-center pt-2 border-t border-gray-100">
|
||||||
|
<div>
|
||||||
|
{property.purpose === "rent" ? (
|
||||||
|
<div>
|
||||||
|
{property.monthlyPrice > 0 && (
|
||||||
|
<span className="text-base font-bold text-amber-600">
|
||||||
|
{Number(property.monthlyPrice).toLocaleString()}
|
||||||
|
<span className="text-xs text-gray-500 font-normal mr-1">{t("perMonth")}</span>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{property.dailyPrice > 0 && !property.monthlyPrice && (
|
||||||
|
<span className="text-base font-bold text-amber-600">
|
||||||
|
{Number(property.dailyPrice).toLocaleString()}
|
||||||
|
<span className="text-xs text-gray-500 font-normal mr-1">{t("perDay")}</span>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{property.deposit > 0 && <div className="text-[10px] text-gray-400">{t("depositAmount", { amount: Number(property.deposit).toLocaleString() })}</div>}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<span className="text-base font-bold text-blue-600">
|
||||||
|
{Number(property.salePrice).toLocaleString()} {t("syp")}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-1">
|
||||||
|
{property.status === "notAvailable" ? (
|
||||||
|
<button
|
||||||
|
onClick={() =>
|
||||||
|
setDeactivateModal({
|
||||||
|
isOpen: true,
|
||||||
|
property,
|
||||||
|
isActivating: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="p-1.5 hover:bg-green-50 rounded-lg transition-colors group"
|
||||||
|
title={t("activateProperty")}
|
||||||
|
>
|
||||||
|
<CheckCircle className="w-3.5 h-3.5 text-gray-400 group-hover:text-green-600" />
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
onClick={() =>
|
||||||
|
setDeactivateModal({
|
||||||
|
isOpen: true,
|
||||||
|
property,
|
||||||
|
isActivating: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="p-1.5 hover:bg-red-50 rounded-lg transition-colors group"
|
||||||
|
title={t("deactivateProperty")}
|
||||||
|
>
|
||||||
|
<XCircle className="w-3.5 h-3.5 text-gray-400 group-hover:text-red-600" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
onClick={() => setViewModal({ isOpen: true, property })}
|
||||||
|
className="px-3 py-1.5 bg-gray-50 hover:bg-blue-50 border border-gray-200 hover:border-blue-200 rounded-lg transition-all group flex items-center gap-1.5"
|
||||||
|
title={t("viewDetails")}
|
||||||
|
>
|
||||||
|
<Eye className="w-3.5 h-3.5 text-gray-400 group-hover:text-blue-600" />
|
||||||
|
<span className="text-xs text-gray-500 group-hover:text-blue-600 font-medium">{t("view")}</span>
|
||||||
|
</button>
|
||||||
|
<button onClick={() => setEditModal({ isOpen: true, property })} className="p-1.5 hover:bg-amber-50 rounded-lg transition-colors group" title={t("editProperty")}>
|
||||||
|
<Edit className="w-3.5 h-3.5 text-gray-400 group-hover:text-amber-600" />
|
||||||
|
</button>
|
||||||
|
<button onClick={() => setDeleteModal({ isOpen: true, property })} className="p-1.5 hover:bg-red-50 rounded-lg transition-colors group" title={t("deleteProperty")}>
|
||||||
|
<Trash2 className="w-3.5 h-3.5 text-gray-400 group-hover:text-red-600" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{property.purpose === "sale" && Number(property.status2) === 0 && (
|
||||||
|
<button
|
||||||
|
onClick={() => openCommissionPayment(property)}
|
||||||
|
className="w-full h-10 bg-amber-600 hover:bg-amber-700 text-white flex items-center justify-center gap-2 font-medium transition-colors"
|
||||||
|
>
|
||||||
|
<CreditCard className="w-4 h-4" />
|
||||||
|
دفع الكمسيون
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{property.purpose === "sale" && Number(property.status2) === 1 && (
|
||||||
|
<div className="w-full h-10 bg-amber-50 text-amber-700 flex items-center justify-center font-medium">قيد الانتظار</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{property.purpose === "sale" && Number(property.status2) === 2 && <div className="w-full h-10 bg-green-50 text-green-700 flex items-center justify-center font-medium">تم التأكيد</div>}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<CommissionPaymentDialog
|
||||||
|
isOpen={commissionPayment.open}
|
||||||
|
property={commissionPayment.property}
|
||||||
|
onClose={() =>
|
||||||
|
setCommissionPayment({
|
||||||
|
open: false,
|
||||||
|
property: null,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
selectedPayment={selectedCommissionPayment}
|
||||||
|
onSelectPayment={setSelectedCommissionPayment}
|
||||||
|
onConfirmPay={handleCommissionPayment}
|
||||||
|
payingId={commissionPayingId}
|
||||||
|
paymentMethods={commissionPaymentMethods}
|
||||||
|
loadingPaymentMethods={loadingCommissionPaymentMethods}
|
||||||
|
receiptImage={commissionReceiptImage}
|
||||||
|
onSetReceiptImage={setCommissionReceiptImage}
|
||||||
|
commissionDetails={commissionDetails}
|
||||||
|
setCommissionDetails={setCommissionDetails}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -692,7 +692,7 @@ export default function PropertiesPage() {
|
|||||||
const filteredProperties = properties
|
const filteredProperties = properties
|
||||||
// العقارات يلي 0 ما تنعرض
|
// العقارات يلي 0 ما تنعرض
|
||||||
.filter((p) => p.purpose === purposeTab)
|
.filter((p) => p.purpose === purposeTab)
|
||||||
.filter((p) => p.purpose === "rent" || p.status === 1)
|
// .filter((p) => p.purpose === "rent" || p.status === 1)
|
||||||
.filter((property) => {
|
.filter((property) => {
|
||||||
if (filters.search && !property.title.includes(filters.search) && !property.description.includes(filters.search)) {
|
if (filters.search && !property.title.includes(filters.search) && !property.description.includes(filters.search)) {
|
||||||
return false;
|
return false;
|
||||||
@ -788,9 +788,9 @@ export default function PropertiesPage() {
|
|||||||
<div className={viewMode === "grid" ? "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6" : "space-y-4"}>
|
<div className={viewMode === "grid" ? "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6" : "space-y-4"}>
|
||||||
{/* شرط مشان ينعرض او لا */}
|
{/* شرط مشان ينعرض او لا */}
|
||||||
{filteredProperties.map((property) =>
|
{filteredProperties.map((property) =>
|
||||||
property.purpose === "rent" || property.status === 1 ? (
|
// property.purpose === "rent" || property.status === 1 ? (
|
||||||
<PropertyCard key={property.id} property={property} viewMode={viewMode} type={property.purpose} onLoginRequired={() => setShowLoginDialog(true)} />
|
<PropertyCard key={property.id} property={property} viewMode={viewMode} type={property.purpose} onLoginRequired={() => setShowLoginDialog(true)} />
|
||||||
) : null,
|
// ) : null,
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -433,11 +433,15 @@ export default function PropertyDetailsPage({ type }) {
|
|||||||
const propInfoId = property._raw?.propertyInformationId || property.id;
|
const propInfoId = property._raw?.propertyInformationId || property.id;
|
||||||
const startDate = new Date(selectedStart + "T00:00:00.000").toISOString();
|
const startDate = new Date(selectedStart + "T00:00:00.000").toISOString();
|
||||||
const endDate = new Date(selectedEnd + "T00:00:00.000").toISOString();
|
const endDate = new Date(selectedEnd + "T00:00:00.000").toISOString();
|
||||||
await bookReservation(propInfoId, startDate, endDate);
|
await bookReservation(propInfoId, startDate, endDate);
|
||||||
|
|
||||||
setBookingSuccess(true);
|
setBookingSuccess(true);
|
||||||
toast.success(t("bookingSentSuccess"));
|
toast.success(t("bookingSentSuccess"));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setBookingError(err.message || t("bookingFailed"));
|
if (err.message=="API 400: Cannot book a reservation for an inactive property."){
|
||||||
|
setBookingError("لا يمكنك حجز هذا العقار لانه غير نشط" || t("bookingFailed"));
|
||||||
|
}
|
||||||
|
|
||||||
} finally {
|
} finally {
|
||||||
setBookingLoading(false);
|
setBookingLoading(false);
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -1,4 +1,6 @@
|
|||||||
|
import { body } from "framer-motion/client";
|
||||||
import AuthService from "../services/AuthService";
|
import AuthService from "../services/AuthService";
|
||||||
|
import { frameData } from "framer-motion";
|
||||||
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";
|
||||||
const REPORT_API_BASE = process.env.NEXT_PUBLIC_REPORT_API_URL || "http://45.93.137.91/api";
|
const REPORT_API_BASE = process.env.NEXT_PUBLIC_REPORT_API_URL || "http://45.93.137.91/api";
|
||||||
|
|
||||||
@ -364,16 +366,38 @@ export async function getSalePropertyById(id) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function updateRentPropertyStatus(id, status) {
|
export async function updateRentPropertyStatus(id, status) {
|
||||||
return apiFetch(`/RentProperties/UpdateStatus/${id}`, {
|
return apiFetch(`/RentProperties/ChangeActivityStatus/${id}`, {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
body: { status },
|
body: status ,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateSalePropertyStatus(id, status) {
|
export async function updateSalePropertyStatus(id, status) {
|
||||||
return apiFetch(`/SaleProperties/UpdateStatus/${id}`, {
|
return apiFetch(`/SaleProperties/ChangeActivityStatus/${id}`, {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
body: { status },
|
body: status ,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function OwnerPaymentForSaleProperite(
|
||||||
|
salePropertyId,
|
||||||
|
PaymentTypeId,
|
||||||
|
TransactionType,
|
||||||
|
paymentImage
|
||||||
|
) {
|
||||||
|
const formData = new FormData();
|
||||||
|
|
||||||
|
formData.append("salePropertyId", String(salePropertyId));
|
||||||
|
formData.append("PaymentTypeId", String(PaymentTypeId));
|
||||||
|
formData.append("TransactionType", String(TransactionType));
|
||||||
|
|
||||||
|
if (paymentImage instanceof File) {
|
||||||
|
formData.append("paymentImage", paymentImage);
|
||||||
|
}
|
||||||
|
|
||||||
|
return apiFetch("/SaleProperties/PaySaleFee", {
|
||||||
|
method: "POST",
|
||||||
|
body: formData,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { initializeApp, getApps } from "firebase/app";
|
import { initializeApp, getApps } from "firebase/app";
|
||||||
import { getMessaging, getToken, onMessage } from "firebase/messaging";
|
import { getMessaging, getToken, onMessage } from "firebase/messaging";
|
||||||
|
import { Cookies } from 'js-cookie';
|
||||||
|
|
||||||
const firebaseConfig = {
|
const firebaseConfig = {
|
||||||
apiKey: "AIzaSyBZV7KBLRJSTApahfrO8lBesmIM3zNRSaY",
|
apiKey: "AIzaSyBZV7KBLRJSTApahfrO8lBesmIM3zNRSaY",
|
||||||
@ -45,8 +46,7 @@ export async function requestNotificationPermission() {
|
|||||||
// Send token to backend
|
// Send token to backend
|
||||||
if (token) {
|
if (token) {
|
||||||
try {
|
try {
|
||||||
const authToken = localStorage.getItem("auth_token");
|
const authToken = Cookies.get(auth_token);
|
||||||
|
|
||||||
if (authToken) {
|
if (authToken) {
|
||||||
const apiBase =
|
const apiBase =
|
||||||
process.env.NEXT_PUBLIC_API_URL ||
|
process.env.NEXT_PUBLIC_API_URL ||
|
||||||
@ -66,7 +66,6 @@ export async function requestNotificationPermission() {
|
|||||||
}
|
}
|
||||||
} catch (err) {}
|
} catch (err) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
return token;
|
return token;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@ -17,7 +17,6 @@ firebase.initializeApp({
|
|||||||
|
|
||||||
//ننشئ رسالة
|
//ننشئ رسالة
|
||||||
const messaging = firebase.messaging();
|
const messaging = firebase.messaging();
|
||||||
|
|
||||||
// Handle background messages
|
// Handle background messages
|
||||||
messaging.onBackgroundMessage((payload) => {
|
messaging.onBackgroundMessage((payload) => {
|
||||||
("[FCM SW] Background message:", payload);
|
("[FCM SW] Background message:", payload);
|
||||||
|
|||||||
Reference in New Issue
Block a user