add payment for SaleProprite ,add commsion,fixed error handling fiexd final things
All checks were successful
Build frontend / build (push) Successful in 1m46s

This commit is contained in:
hamzaobed7
2026-08-18 01:24:18 +03:00
parent 5592d9fbd2
commit 3472dc1175
10 changed files with 793 additions and 1070 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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,
}); });
} }

View File

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

View File

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