1880 lines
83 KiB
JavaScript
1880 lines
83 KiB
JavaScript
"use client";
|
||
|
||
import { useState, useEffect } from "react";
|
||
import { motion, AnimatePresence } from "framer-motion";
|
||
import { useRouter } from "next/navigation";
|
||
import Link from "next/link";
|
||
import { useTranslation } from "react-i18next";
|
||
import {
|
||
PlusCircle,
|
||
Building,
|
||
Home,
|
||
DollarSign,
|
||
MapPin,
|
||
Bed,
|
||
Bath,
|
||
Square,
|
||
Edit,
|
||
Trash2,
|
||
Eye,
|
||
Calendar,
|
||
TrendingUp,
|
||
Users,
|
||
FileText,
|
||
Image as ImageIcon,
|
||
CheckCircle,
|
||
XCircle,
|
||
AlertCircle,
|
||
ChevronRight,
|
||
ChevronLeft,
|
||
Loader2,
|
||
Clock,
|
||
Wifi,
|
||
Zap,
|
||
Flame,
|
||
Droplets,
|
||
Cigarette,
|
||
Dog,
|
||
Music,
|
||
Warehouse,
|
||
Layers,
|
||
Sofa,
|
||
DoorOpen,
|
||
Wind,
|
||
Pencil,
|
||
Save,
|
||
X,
|
||
Star,
|
||
Ban,
|
||
Check,
|
||
School,
|
||
Hospital,
|
||
Store,
|
||
GraduationCap,
|
||
TreePine,
|
||
} from "lucide-react";
|
||
import toast, { Toaster } from "react-hot-toast";
|
||
import AuthService from "../../services/AuthService";
|
||
import { buildRentPropertyPayload, getMyRentListings, getMySaleListings, editRentProperty, editSaleProperty, updateRentPropertyStatus, updateSalePropertyStatus, uploadPicture } from "../../utils/api";
|
||
import { PropertyService } from "../../enums/PropertyService";
|
||
import { PropertyTerm } from "../../enums/PropertyTerm";
|
||
import Loading from "@/app/loading";
|
||
|
||
const serviceI18nKeys = {
|
||
[PropertyService.ELECTRICITY]: "propertyService.electricity",
|
||
[PropertyService.INTERNET]: "propertyService.internet",
|
||
[PropertyService.HEATING]: "propertyService.heating",
|
||
[PropertyService.WATER]: "propertyService.water",
|
||
[PropertyService.POOL]: "propertyService.pool",
|
||
[PropertyService.PRIVATE_GARDEN]: "propertyService.privateGarden",
|
||
[PropertyService.PARKING]: "propertyService.parking",
|
||
[PropertyService.SECURITY_247]: "propertyService.security247",
|
||
[PropertyService.CENTRAL_HEATING]: "propertyService.centralHeating",
|
||
[PropertyService.CENTRAL_AIR_CONDITIONING]: "propertyService.centralAirConditioning",
|
||
[PropertyService.EQUIPPED_KITCHEN]: "propertyService.equippedKitchen",
|
||
[PropertyService.MAIDS_ROOM]: "propertyService.maidsRoom",
|
||
[PropertyService.ELEVATOR]: "propertyService.elevator",
|
||
};
|
||
|
||
const termI18nKeys = {
|
||
[PropertyTerm.NO_SMOKING]: "propertyTerm.noSmoking",
|
||
[PropertyTerm.NO_ANIMALS]: "propertyTerm.noAnimals",
|
||
[PropertyTerm.NO_PARTIES]: "propertyTerm.noParties",
|
||
};
|
||
|
||
const proximityI18nKeys = {
|
||
School: "proximity.school",
|
||
Hospital: "proximity.hospital",
|
||
Restaurant: "proximity.restaurant",
|
||
University: "proximity.university",
|
||
Park: "proximity.park",
|
||
Mall: "proximity.mall",
|
||
Supermarket: "proximity.supermarket",
|
||
Pharmacy: "proximity.pharmacy",
|
||
Mosque: "proximity.mosque",
|
||
Bank: "proximity.bank",
|
||
Airport: "proximity.airport",
|
||
BusStation: "proximity.busStation",
|
||
};
|
||
|
||
const DeleteConfirmationModal = ({ isOpen, onClose, onConfirm, propertyTitle }) => {
|
||
const { t } = useTranslation();
|
||
if (!isOpen) return null;
|
||
|
||
return (
|
||
<motion.div
|
||
initial={{ opacity: 0 }}
|
||
animate={{ opacity: 1 }}
|
||
exit={{ opacity: 0 }}
|
||
className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center p-4 z-50"
|
||
onClick={onClose}
|
||
>
|
||
<motion.div
|
||
initial={{ scale: 0.9, y: 20 }}
|
||
animate={{ scale: 1, y: 0 }}
|
||
exit={{ scale: 0.9, y: 20 }}
|
||
className="bg-white rounded-2xl w-full max-w-md p-6 shadow-2xl"
|
||
onClick={(e) => e.stopPropagation()}
|
||
>
|
||
<div className="text-center mb-4">
|
||
<div className="w-16 h-16 bg-red-100 rounded-full flex items-center justify-center mx-auto mb-3">
|
||
<AlertCircle className="w-8 h-8 text-red-600" />
|
||
</div>
|
||
<h3 className="text-xl font-bold text-gray-900">{t("deleteConfirmTitle")}</h3>
|
||
<p className="text-sm text-gray-500 mt-2">{t("deleteConfirmMessage", { title: propertyTitle })}</p>
|
||
<p className="text-xs text-red-500 mt-1">{t("deleteIrreversible")}</p>
|
||
</div>
|
||
|
||
<div className="flex gap-3 pt-3">
|
||
<button onClick={onClose} className="flex-1 bg-gray-100 text-gray-700 py-3 rounded-xl font-medium hover:bg-gray-200 transition-colors">
|
||
{t("cancel")}
|
||
</button>
|
||
<button onClick={onConfirm} className="flex-1 bg-red-600 text-white py-3 rounded-xl font-medium hover:bg-red-700 transition-colors">
|
||
{t("confirmDelete")}
|
||
</button>
|
||
</div>
|
||
</motion.div>
|
||
</motion.div>
|
||
);
|
||
};
|
||
|
||
const DeactivateConfirmationModal = ({ isOpen, onClose, onConfirm, propertyTitle, isActivating }) => {
|
||
const { t } = useTranslation();
|
||
if (!isOpen) return null;
|
||
|
||
return (
|
||
<motion.div
|
||
initial={{ opacity: 0 }}
|
||
animate={{ opacity: 1 }}
|
||
exit={{ opacity: 0 }}
|
||
className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center p-4 z-50"
|
||
onClick={onClose}
|
||
>
|
||
<motion.div
|
||
initial={{ scale: 0.9, y: 20 }}
|
||
animate={{ scale: 1, y: 0 }}
|
||
exit={{ scale: 0.9, y: 20 }}
|
||
className="bg-white rounded-2xl w-full max-w-md p-6 shadow-2xl"
|
||
onClick={(e) => e.stopPropagation()}
|
||
>
|
||
<div className="text-center mb-4">
|
||
<div className="w-16 h-16 bg-amber-100 rounded-full flex items-center justify-center mx-auto mb-3">
|
||
<AlertCircle className="w-8 h-8 text-amber-600" />
|
||
</div>
|
||
<h3 className="text-xl font-bold text-gray-900">{isActivating ? t("activateTitle") : t("deactivateTitle")}</h3>
|
||
<p className="text-sm text-gray-500 mt-2">
|
||
<span className="font-bold text-gray-700">"{propertyTitle}"</span>
|
||
</p>
|
||
{!isActivating ? (
|
||
<div className="mt-4 p-4 bg-red-50 border border-red-200 rounded-xl">
|
||
<p className="text-sm text-red-700 font-medium">⚠️ {t("warning")}</p>
|
||
<p className="text-sm text-red-600 mt-1">{t("deactivateWarning")}</p>
|
||
</div>
|
||
) : (
|
||
<div className="mt-4 p-4 bg-green-50 border border-green-200 rounded-xl">
|
||
<p className="text-sm text-green-700 font-medium">✅ {t("activateInfoTitle")}</p>
|
||
<p className="text-sm text-green-600 mt-1">{t("activateInfo")}</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
<div className="flex gap-3 pt-3">
|
||
<button onClick={onClose} className="flex-1 bg-gray-100 text-gray-700 py-3 rounded-xl font-medium hover:bg-gray-200 transition-colors">
|
||
{t("cancel")}
|
||
</button>
|
||
<button
|
||
onClick={onConfirm}
|
||
className={`flex-1 py-3 rounded-xl font-medium transition-colors ${isActivating ? "bg-green-600 text-white hover:bg-green-700" : "bg-red-600 text-white hover:bg-red-700"}`}
|
||
>
|
||
{isActivating ? t("confirmActivate") : t("confirmDeactivate")}
|
||
</button>
|
||
</div>
|
||
</motion.div>
|
||
</motion.div>
|
||
);
|
||
};
|
||
|
||
const PropertyViewModal = ({ isOpen, onClose, property }) => {
|
||
const { t } = useTranslation();
|
||
if (!isOpen || !property) return null;
|
||
|
||
return (
|
||
<motion.div
|
||
initial={{ opacity: 0 }}
|
||
animate={{ opacity: 1 }}
|
||
exit={{ opacity: 0 }}
|
||
className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center p-4 z-50"
|
||
onClick={onClose}
|
||
>
|
||
<motion.div
|
||
initial={{ scale: 0.9, y: 20 }}
|
||
animate={{ scale: 1, y: 0 }}
|
||
exit={{ scale: 0.9, y: 20 }}
|
||
className="bg-white rounded-2xl w-full max-w-4xl max-h-[90vh] overflow-y-auto shadow-2xl"
|
||
onClick={(e) => e.stopPropagation()}
|
||
>
|
||
<div className="sticky top-0 z-10 bg-gradient-to-r from-amber-500 to-amber-600 p-6 text-white flex justify-between items-center">
|
||
<div>
|
||
<h2 className="text-2xl font-bold">{property.title}</h2>
|
||
<p className="text-amber-100 text-sm mt-1">
|
||
{t("addedOn")}
|
||
{new Date(property.createdAt).toLocaleDateString("ar-SA")}
|
||
</p>
|
||
</div>
|
||
<button onClick={onClose} className="p-2 hover:bg-white/20 rounded-full transition-colors">
|
||
<XCircle className="w-6 h-6" />
|
||
</button>
|
||
</div>
|
||
|
||
<div className="p-6">
|
||
{property.images && property.images.length > 0 && (
|
||
<div className="mb-6">
|
||
<h3 className="text-lg font-bold text-gray-900 mb-3">{t("propertyImages")}</h3>
|
||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||
{property.images.map((image, index) => (
|
||
<div key={index} className="relative aspect-square rounded-lg overflow-hidden border border-gray-200">
|
||
<img src={image} alt={t("imageAlt", { title: property.title, index: index + 1 })} className="w-full h-full object-cover" />
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
|
||
<div className="bg-gray-50 p-4 rounded-xl">
|
||
<h4 className="font-bold text-gray-700 mb-3">{t("basicInfo")}</h4>
|
||
<div className="space-y-2">
|
||
<p className="text-sm">
|
||
<span className="text-gray-500">{t("propertyTypeLabel")}</span>
|
||
<span className="font-medium text-gray-900 mr-2">{property.propertyTypeLabel || t("offer")}</span>
|
||
</p>
|
||
<p className="text-sm">
|
||
<span className="text-gray-500">{t("displayTypeLabel")}</span>
|
||
<span className="font-medium text-gray-900 mr-2">
|
||
{property.displayType === "Daily rent"
|
||
? t("dailyRent")
|
||
: property.displayType === "Monthly rent"
|
||
? t("monthlyRent")
|
||
: property.displayType === "Both"
|
||
? t("dailyAndMonthly")
|
||
: property.displayType === "For sale"
|
||
? t("forSale")
|
||
: property.rentType === "daily"
|
||
? t("dailyRent")
|
||
: property.rentType === "monthly"
|
||
? t("monthlyRent")
|
||
: property.dailyPrice > 0 && property.monthlyPrice > 0
|
||
? t("dailyAndMonthly")
|
||
: property.monthlyPrice > 0
|
||
? t("monthlyRent")
|
||
: property.dailyPrice > 0
|
||
? t("dailyRent")
|
||
: property.purpose === "sale"
|
||
? t("forSale")
|
||
: t("offer")}
|
||
</span>
|
||
</p>
|
||
{property.purpose === "rent" && (
|
||
<p className="text-sm">
|
||
<span className="text-gray-500">{t("furnishedStatusLabel")}</span>
|
||
<span className={`font-medium mr-2 ${property.furnished ? "text-green-600" : "text-gray-600"}`}>{property.furnished ? t("furnished") : t("unfurnished")}</span>
|
||
</p>
|
||
)}
|
||
<p className="text-sm">
|
||
<span className="text-gray-500">{t("propertyStatusLabel")}</span>
|
||
<span className={`font-medium mr-2 ${property.status === "available" ? "text-green-600" : "text-yellow-600"}`}>{property.status === "available" ? t("available") : t("rented")}</span>
|
||
</p>
|
||
{property.description && (
|
||
<p className="text-sm mt-2">
|
||
<span className="text-gray-500">{t("descriptionLabel")}</span>
|
||
<span className="text-gray-700 mr-2">{property.description}</span>
|
||
</p>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
<div className="bg-gray-50 p-4 rounded-xl">
|
||
<h4 className="font-bold text-gray-700 mb-3">{t("basicInfo")}</h4>
|
||
<div className="grid grid-cols-4 gap-2">
|
||
<div className="text-center">
|
||
<Bed className="w-5 h-5 text-amber-500 mx-auto mb-1" />
|
||
<span className="text-sm font-bold">{property.bedrooms}</span>
|
||
<span className="text-xs text-gray-500 block">{t("rooms")}</span>
|
||
</div>
|
||
<div className="text-center">
|
||
<Bath className="w-5 h-5 text-amber-500 mx-auto mb-1" />
|
||
<span className="text-sm font-bold">{property.bathrooms}</span>
|
||
<span className="text-xs text-gray-500 block">{t("bathrooms")}</span>
|
||
</div>
|
||
<div className="text-center">
|
||
<Square className="w-5 h-5 text-amber-500 mx-auto mb-1" />
|
||
<span className="text-sm font-bold">{property.area}</span>
|
||
<span className="text-xs text-gray-500 block">{t("sqm")}</span>
|
||
</div>
|
||
{property.floor > 0 && (
|
||
<div className="text-center">
|
||
<Layers className="w-5 h-5 text-amber-500 mx-auto mb-1" />
|
||
<span className="text-sm font-bold">{property.floor}</span>
|
||
<span className="text-xs text-gray-500 block">{t("floor")}</span>
|
||
</div>
|
||
)}
|
||
{property.salons > 0 && (
|
||
<div className="text-center">
|
||
<Sofa className="w-5 h-5 text-amber-500 mx-auto mb-1" />
|
||
<span className="text-sm font-bold">{property.salons}</span>
|
||
<span className="text-xs text-gray-500 block">{t("salons")}</span>
|
||
</div>
|
||
)}
|
||
{property.balconies > 0 && (
|
||
<div className="text-center">
|
||
<DoorOpen className="w-5 h-5 text-amber-500 mx-auto mb-1" />
|
||
<span className="text-sm font-bold">{property.balconies}</span>
|
||
<span className="text-xs text-gray-500 block">{t("balconies")}</span>
|
||
</div>
|
||
)}
|
||
{property.bookedCount > 0 && (
|
||
<div className="text-center">
|
||
<Calendar className="w-5 h-5 text-amber-500 mx-auto mb-1" />
|
||
<span className="text-sm font-bold">{property.bookedCount}</span>
|
||
<span className="text-xs text-gray-500 block">{t("bookings")}</span>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="bg-gray-50 p-4 rounded-xl mb-6">
|
||
<h4 className="font-bold text-gray-700 mb-3 flex items-center gap-2">
|
||
<MapPin className="w-5 h-5 text-amber-500" />
|
||
{t("location")}
|
||
</h4>
|
||
<p className="text-gray-700">
|
||
{property.address || t("addressNotSpecified")}
|
||
{property.city && `، ${property.city}`}
|
||
{property.district && `، ${property.district}`}
|
||
</p>
|
||
</div>
|
||
|
||
{property.services && (Array.isArray(property.services) ? property.services.length > 0 : Object.keys(property.services).length > 0) && (
|
||
<div className="bg-gray-50 p-4 rounded-xl mb-6">
|
||
<h4 className="font-bold text-gray-700 mb-3">{t("availableServices")}</h4>
|
||
<div className="flex flex-wrap gap-2">
|
||
{Array.isArray(property.services)
|
||
? property.services.map((svc, i) => (
|
||
<span key={i} className="inline-flex items-center gap-1 px-3 py-1 bg-green-100 text-green-800 rounded-full text-sm">
|
||
{t(serviceI18nKeys[svc]) || svc}
|
||
</span>
|
||
))
|
||
: Object.entries(property.services).map(([key, value]) => {
|
||
if (!value) return null;
|
||
const detail = typeof value === "object" && value.detail ? value.detail : typeof value === "string" ? value : null;
|
||
return (
|
||
<span key={key} className="inline-flex items-center gap-1 px-3 py-1 bg-green-100 text-green-800 rounded-full text-sm">
|
||
{t(serviceI18nKeys[key]) || key}
|
||
{detail && <span className="text-green-400">· {detail}</span>}
|
||
</span>
|
||
);
|
||
})}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{property.proximity && Object.keys(property.proximity).length > 0 && (
|
||
<div className="bg-gray-50 p-4 rounded-xl mb-6">
|
||
<h4 className="font-bold text-gray-700 mb-3">{t("proximityToServices")}</h4>
|
||
<div className="grid grid-cols-2 md:grid-cols-3 gap-2">
|
||
{Object.entries(property.proximity).map(([key, val]) => {
|
||
if (!val) return null;
|
||
const dist = typeof val === "object" ? val.distance : val;
|
||
return (
|
||
<div key={key} className="flex items-center gap-2 p-2 bg-white rounded-lg">
|
||
{key === "School" && <School className="w-4 h-4 text-amber-500" />}
|
||
{key === "Hospital" && <Hospital className="w-4 h-4 text-amber-500" />}
|
||
{key === "Restaurant" && <Store className="w-4 h-4 text-amber-500" />}
|
||
{key === "University" && <GraduationCap className="w-4 h-4 text-amber-500" />}
|
||
{key === "Park" && <TreePine className="w-4 h-4 text-amber-500" />}
|
||
{key === "Mall" && <Building className="w-4 h-4 text-amber-500" />}
|
||
{!["School", "Hospital", "Restaurant", "University", "Park", "Mall"].includes(key) && <MapPin className="w-4 h-4 text-amber-500" />}
|
||
<div>
|
||
<span className="text-sm text-gray-700">{t(proximityI18nKeys[key]) || key}</span>
|
||
<span className="text-xs text-gray-500 mr-1">
|
||
{dist} {typeof dist === "number" ? t("km") : ""}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{property.terms && Object.keys(property.terms).length > 0 && (
|
||
<div className="bg-gray-50 p-4 rounded-xl mb-6">
|
||
<h4 className="font-bold text-gray-700 mb-3">{t("termsOfUse")}</h4>
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
|
||
{Object.entries(property.terms).map(([key, value]) => {
|
||
if (!value) return null;
|
||
return (
|
||
<div key={key} className="flex items-center gap-2 p-2 bg-white rounded-lg">
|
||
{key.startsWith("No") || key.startsWith("Only") ? <Ban className="w-4 h-4 text-red-500 flex-shrink-0" /> : <Check className="w-4 h-4 text-green-500 flex-shrink-0" />}
|
||
<span className="text-sm text-gray-700">{t(termI18nKeys[key]) || key}</span>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
<div className="bg-amber-50 p-4 rounded-xl">
|
||
<h4 className="font-bold text-amber-700 mb-3">{t("priceInfo")}</h4>
|
||
{property.purpose === "rent" ? (
|
||
<div className="space-y-2">
|
||
{property.dailyPrice > 0 && (
|
||
<p className="flex justify-between">
|
||
<span className="text-gray-600">{t("dailyPriceLabel")}</span>
|
||
<span className="font-bold text-amber-600">
|
||
{Number(property.dailyPrice).toLocaleString()} {t("syp")}
|
||
</span>
|
||
</p>
|
||
)}
|
||
{property.monthlyPrice > 0 && (
|
||
<p className="flex justify-between">
|
||
<span className="text-gray-600">{t("monthlyPriceLabel")}</span>
|
||
<span className="font-bold text-amber-600">
|
||
{Number(property.monthlyPrice).toLocaleString()} {t("syp")}
|
||
</span>
|
||
</p>
|
||
)}
|
||
{property.deposit > 0 && (
|
||
<p className="flex justify-between">
|
||
<span className="text-gray-600">{t("depositLabel")}</span>
|
||
<span className="font-bold text-gray-700">
|
||
{Number(property.deposit).toLocaleString()} {t("syp")}
|
||
</span>
|
||
</p>
|
||
)}
|
||
<p className="text-sm text-gray-500 mt-1">
|
||
{t("rentTypeLabel")} {property.rentType === "daily" ? t("daily") : property.rentType === "monthly" ? t("monthly") : t("dailyAndMonthly")}
|
||
</p>
|
||
{property.rating > 0 && (
|
||
<p className="flex justify-between mt-2 pt-2 border-t border-amber-200">
|
||
<span className="text-gray-600">{t("ratingLabel")}</span>
|
||
<span className="font-bold text-amber-600 flex items-center gap-1">
|
||
{Number(property.rating).toFixed(1)} <Star className="w-4 h-4 fill-amber-500" />
|
||
</span>
|
||
</p>
|
||
)}
|
||
</div>
|
||
) : (
|
||
<p className="flex justify-between">
|
||
<span className="text-gray-600">{t("salePriceLabel")}</span>
|
||
<span className="font-bold text-blue-600">
|
||
{Number(property.salePrice).toLocaleString()} {t("syp")}
|
||
</span>
|
||
</p>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</motion.div>
|
||
</motion.div>
|
||
);
|
||
};
|
||
|
||
const PropertyEditModal = ({ isOpen, onClose, property, onSave }) => {
|
||
const { t } = useTranslation();
|
||
const [formData, setFormData] = useState({
|
||
propertyType: "apartment",
|
||
furnished: false,
|
||
description: "",
|
||
bedrooms: 0,
|
||
bathrooms: 0,
|
||
floor: 0,
|
||
salons: 0,
|
||
balconies: 0,
|
||
livingRooms: 0,
|
||
area: 0,
|
||
services: {},
|
||
serviceDetails: {},
|
||
terms: {},
|
||
customTerms: [],
|
||
nearbySchool: "",
|
||
nearbyHospital: "",
|
||
nearbyRestaurant: "",
|
||
nearbyUniversity: "",
|
||
nearbyPark: "",
|
||
nearbyMall: "",
|
||
purpose: "rent",
|
||
currencyId: 1,
|
||
dailyPrice: 0,
|
||
monthlyPrice: 0,
|
||
deposit: 0,
|
||
rentType: "monthly",
|
||
allowedPaymentPeriod: "",
|
||
salePrice: 0,
|
||
});
|
||
const [newCustomTerm, setNewCustomTerm] = useState("");
|
||
const [isSaving, setIsSaving] = useState(false);
|
||
const [existingImages, setExistingImages] = useState([]);
|
||
const [newImages, setNewImages] = useState([]);
|
||
const editApiBase = process.env.NEXT_PUBLIC_API_URL || "https://45.93.137.91.nip.io/api";
|
||
|
||
const editImgUrl = (path) => {
|
||
if (!path) return null;
|
||
if (path.startsWith("http://") || path.startsWith("https://")) return path;
|
||
return `${editApiBase}${path}`;
|
||
};
|
||
|
||
useEffect(() => {
|
||
if (!property || !isOpen) return;
|
||
const raw = property._raw || {};
|
||
const info = raw.propertyInformation || {};
|
||
let details = {};
|
||
try {
|
||
details = typeof info.detailsJSON === "object" && info.detailsJSON ? info.detailsJSON : JSON.parse(info.detailsJSON || "{}");
|
||
} catch {
|
||
details = {};
|
||
}
|
||
|
||
const propServices = property.services || {};
|
||
const services = {};
|
||
const serviceDetails = {};
|
||
|
||
Object.keys(serviceI18nKeys).forEach((key) => {
|
||
const val = propServices[key];
|
||
if (val && typeof val === "string") {
|
||
services[key] = true;
|
||
serviceDetails[key] = val;
|
||
} else if (val && typeof val === "object" && val.detail) {
|
||
services[key] = true;
|
||
serviceDetails[key] = val.detail;
|
||
} else {
|
||
services[key] = !!val;
|
||
serviceDetails[key] = details.serviceDetails?.[key] || "";
|
||
}
|
||
});
|
||
|
||
const rawSvcArray = Array.isArray(details.services) ? details.services : [];
|
||
rawSvcArray.forEach((key) => {
|
||
if (!(key in services)) {
|
||
services[key] = true;
|
||
serviceDetails[key] = details.serviceDetails?.[key] || "";
|
||
}
|
||
});
|
||
|
||
const propTerms = property.terms || {};
|
||
const terms = {};
|
||
Object.keys(termI18nKeys).forEach((key) => {
|
||
terms[key] = !!propTerms[key];
|
||
});
|
||
if (details.terms && typeof details.terms === "object") {
|
||
Object.entries(details.terms).forEach(([key, val]) => {
|
||
if (val && !(key in terms)) {
|
||
terms[key] = true;
|
||
}
|
||
});
|
||
}
|
||
|
||
const prox = property.proximity || details.nearbyDistances || {};
|
||
|
||
setFormData({
|
||
propertyType: property.propertyType || "apartment",
|
||
furnished: property.furnished ?? false,
|
||
description: property.description || details.description || "",
|
||
bedrooms: property.bedrooms || 0,
|
||
bathrooms: property.bathrooms || 0,
|
||
floor: property.floor ?? details.floorNumber ?? details.floor ?? 0,
|
||
salons: property.salons ?? details.numberOfSalons ?? details.salons ?? 0,
|
||
balconies: property.balconies ?? details.numberOfBalconies ?? details.balconies ?? 0,
|
||
livingRooms: property.livingRooms ?? details.numberOfLivingRooms ?? details.livingRooms ?? 0,
|
||
area: property.area || 0,
|
||
services,
|
||
serviceDetails,
|
||
terms,
|
||
customTerms: details.customTerms || [],
|
||
nearbySchool: prox.School ?? prox.school ?? prox.nearbySchool ?? "",
|
||
nearbyHospital: prox.Hospital ?? prox.hospital ?? prox.nearbyHospital ?? "",
|
||
nearbyRestaurant: prox.Restaurant ?? prox.restaurant ?? prox.nearbyRestaurant ?? "",
|
||
nearbyUniversity: prox.University ?? prox.university ?? prox.nearbyUniversity ?? "",
|
||
nearbyPark: prox.Park ?? prox.park ?? prox.nearbyPark ?? "",
|
||
nearbyMall: prox.Mall ?? prox.mall ?? prox.nearbyMall ?? "",
|
||
purpose: property.purpose || "rent",
|
||
currencyId: property.currencyId || 1,
|
||
...(property.purpose === "rent"
|
||
? {
|
||
dailyPrice: property.dailyPrice || 0,
|
||
monthlyPrice: property.monthlyPrice || 0,
|
||
deposit: property.deposit || 0,
|
||
rentType: property.rentType || "monthly",
|
||
allowedPaymentPeriod: property.allowedPaymentPeriod || details.allowedPaymentPeriod || "",
|
||
}
|
||
: { salePrice: property.salePrice || 0 }),
|
||
});
|
||
setNewCustomTerm("");
|
||
setNewImages([]);
|
||
const rawImages = Array.isArray(property.images) ? property.images : Array.isArray(info.images) ? info.images : [];
|
||
setExistingImages(rawImages.filter((p) => p && p.trim()));
|
||
}, [property, isOpen]);
|
||
|
||
const handleChange = (field, value) => {
|
||
setFormData((prev) => ({ ...prev, [field]: value }));
|
||
};
|
||
|
||
const handleServiceToggle = (key, checked) => {
|
||
setFormData((prev) => ({
|
||
...prev,
|
||
services: { ...prev.services, [key]: checked },
|
||
}));
|
||
};
|
||
|
||
const handleServiceDetail = (key, value) => {
|
||
setFormData((prev) => ({
|
||
...prev,
|
||
serviceDetails: { ...prev.serviceDetails, [key]: value },
|
||
}));
|
||
};
|
||
|
||
const handleTermToggle = (key, checked) => {
|
||
setFormData((prev) => ({
|
||
...prev,
|
||
terms: { ...prev.terms, [key]: checked },
|
||
}));
|
||
};
|
||
|
||
const addCustomTerm = () => {
|
||
const term = newCustomTerm.trim();
|
||
if (!term) return;
|
||
setFormData((prev) => ({
|
||
...prev,
|
||
customTerms: [...(prev.customTerms || []), term],
|
||
}));
|
||
setNewCustomTerm("");
|
||
};
|
||
|
||
const removeCustomTerm = (index) => {
|
||
setFormData((prev) => ({
|
||
...prev,
|
||
customTerms: prev.customTerms.filter((_, i) => i !== index),
|
||
}));
|
||
};
|
||
|
||
const handleRemoveExistingImage = (index) => {
|
||
setExistingImages((prev) => prev.filter((_, i) => i !== index));
|
||
};
|
||
|
||
const handleMoveExistingImage = (index, direction) => {
|
||
setExistingImages((prev) => {
|
||
const next = [...prev];
|
||
const target = index + direction;
|
||
if (target < 0 || target >= next.length) return prev;
|
||
[next[index], next[target]] = [next[target], next[index]];
|
||
return next;
|
||
});
|
||
};
|
||
|
||
const handleAddImages = async (files) => {
|
||
const validFiles = Array.from(files).filter((f) => {
|
||
if (!f.type.startsWith("image/")) return false;
|
||
if (f.size > 5 * 1024 * 1024) {
|
||
toast.error(t("fileTooLarge") || "الصورة كبيرة جداً");
|
||
return false;
|
||
}
|
||
return true;
|
||
});
|
||
if (validFiles.length === 0) return;
|
||
const maxTotal = 10;
|
||
const currentCount = existingImages.length + newImages.length;
|
||
const slots = maxTotal - currentCount;
|
||
if (slots <= 0) {
|
||
toast.error(t("maxImagesReached") || "الحد الأقصى 10 صور");
|
||
return;
|
||
}
|
||
const toAdd = validFiles.slice(0, slots);
|
||
const entries = toAdd.map((file) => ({
|
||
file,
|
||
preview: URL.createObjectURL(file),
|
||
path: null,
|
||
uploading: true,
|
||
}));
|
||
setNewImages((prev) => [...prev, ...entries]);
|
||
for (let i = 0; i < entries.length; i++) {
|
||
const entry = entries[i];
|
||
try {
|
||
const result = await uploadPicture(entry.file);
|
||
const path = result?.value || result?.path || result?.url || result;
|
||
entry.path = path;
|
||
} catch {
|
||
toast.error(t("uploadFailed") || "فشل رفع الصورة");
|
||
} finally {
|
||
entry.uploading = false;
|
||
setNewImages((prev) => [...prev]);
|
||
}
|
||
}
|
||
};
|
||
|
||
const handleRemoveNewImage = (index) => {
|
||
const entry = newImages[index];
|
||
if (entry?.preview) URL.revokeObjectURL(entry.preview);
|
||
setNewImages((prev) => prev.filter((_, i) => i !== index));
|
||
};
|
||
|
||
const handleMoveNewImage = (index, direction) => {
|
||
setNewImages((prev) => {
|
||
const next = [...prev];
|
||
const target = index + direction;
|
||
if (target < 0 || target >= next.length) return prev;
|
||
[next[index], next[target]] = [next[target], next[index]];
|
||
return next;
|
||
});
|
||
};
|
||
|
||
const handleSave = async () => {
|
||
const errors = [];
|
||
if (!formData.description?.trim()) errors.push(t("descriptionRequired") || "الوصف مطلوب");
|
||
if (formData.bedrooms < 1) errors.push(t("bedroomsRequired") || "يجب أن يكون عدد الغرف على الأقل 1");
|
||
if (formData.bathrooms < 1) errors.push(t("bathroomsRequired") || "يجب أن يكون عدد الحمامات على الأقل 1");
|
||
if (!formData.area || formData.area <= 0) errors.push(t("areaRequired") || "المساحة مطلوبة");
|
||
if (formData.purpose === "rent") {
|
||
if ((!formData.dailyPrice || formData.dailyPrice <= 0) && (!formData.monthlyPrice || formData.monthlyPrice <= 0)) {
|
||
errors.push(t("priceRequired") || "يجب إدخال سعر الأجر الشهري أو اليومي");
|
||
}
|
||
} else if (formData.purpose === "sale") {
|
||
if (!formData.salePrice || formData.salePrice <= 0) errors.push(t("priceRequired") || "السعر مطلوب");
|
||
}
|
||
if (errors.length > 0) {
|
||
toast.error(errors[0]);
|
||
return;
|
||
}
|
||
setIsSaving(true);
|
||
try {
|
||
const uploadedPaths = newImages.filter((e) => e.path && !e.uploading).map((e) => e.path);
|
||
const allImages = [...existingImages, ...uploadedPaths];
|
||
await onSave({ ...formData, _images: allImages });
|
||
} catch {
|
||
setIsSaving(false);
|
||
}
|
||
};
|
||
|
||
if (!isOpen || !property) return null;
|
||
|
||
return (
|
||
<motion.div
|
||
initial={{ opacity: 0 }}
|
||
animate={{ opacity: 1 }}
|
||
exit={{ opacity: 0 }}
|
||
className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center p-4 z-50"
|
||
onClick={onClose}
|
||
>
|
||
<motion.div
|
||
initial={{ scale: 0.9, y: 20 }}
|
||
animate={{ scale: 1, y: 0 }}
|
||
exit={{ scale: 0.9, y: 20 }}
|
||
className="bg-white rounded-2xl w-full max-w-4xl max-h-[90vh] flex flex-col shadow-2xl"
|
||
onClick={(e) => e.stopPropagation()}
|
||
>
|
||
<div className="sticky top-0 z-10 bg-gradient-to-r from-amber-500 to-amber-600 p-6 text-white flex justify-between items-center">
|
||
<div>
|
||
<h2 className="text-2xl font-bold">{t("editProperty")}</h2>
|
||
<p className="text-amber-100 text-sm mt-1">{t("editSubtitle")}</p>
|
||
</div>
|
||
<button onClick={onClose} className="p-2 hover:bg-white/20 rounded-full transition-colors">
|
||
<XCircle className="w-6 h-6" />
|
||
</button>
|
||
</div>
|
||
|
||
<div className="p-6 space-y-6 overflow-y-auto flex-1">
|
||
{/* Basic Info */}
|
||
<div className="bg-gray-50 p-4 rounded-xl">
|
||
<h3 className="text-lg font-bold text-gray-900 mb-4">{t("basicInfo")}</h3>
|
||
<div className="space-y-4">
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">{t("propertyTypeLabel")}</label>
|
||
<select
|
||
value={formData.propertyType}
|
||
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"
|
||
>
|
||
<option value="apartment">{t("buildingType.apartment")}</option>
|
||
<option value="villa">{t("buildingType.villa")}</option>
|
||
<option value="sweet">{t("buildingType.sweet")}</option>
|
||
<option value="room">{t("buildingType.room")}</option>
|
||
<option value="studio">{t("buildingType.studio")}</option>
|
||
<option value="office">{t("buildingType.office")}</option>
|
||
<option value="farms">{t("buildingType.farms")}</option>
|
||
<option value="shop">{t("buildingType.shop")}</option>
|
||
<option value="warehouse">{t("buildingType.warehouse")}</option>
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-2">{t("furnishedStatusLabel")}</label>
|
||
<div className="flex gap-4">
|
||
{[
|
||
{ value: true, label: t("furnished") },
|
||
{ value: false, label: t("unfurnished") },
|
||
].map((opt) => (
|
||
<label key={String(opt.value)} className="flex items-center gap-2 cursor-pointer">
|
||
<input type="radio" name="furnished" checked={formData.furnished === opt.value} onChange={() => handleChange("furnished", opt.value)} className="w-4 h-4 text-amber-500" />
|
||
<span className="text-sm">{opt.label}</span>
|
||
</label>
|
||
))}
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">{t("descriptionLabel")}</label>
|
||
<textarea
|
||
value={formData.description}
|
||
onChange={(e) => handleChange("description", 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"
|
||
rows={3}
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Details */}
|
||
<div className="bg-gray-50 p-4 rounded-xl">
|
||
<h3 className="text-lg font-bold text-gray-900 mb-4">{t("basicInfo")}</h3>
|
||
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
|
||
{[
|
||
{ id: "bedrooms", label: t("bedroomsCount") },
|
||
{ id: "bathrooms", label: t("bathroomsCount") },
|
||
{ id: "livingRooms", label: t("livingRoomsCount") },
|
||
{ id: "floor", label: t("floorLabel") },
|
||
{ id: "salons", label: t("salonsCount") },
|
||
{ id: "balconies", label: t("balconiesCount") },
|
||
{ id: "area", label: t("areaSqm") },
|
||
].map((field) => (
|
||
<div key={field.id}>
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">{field.label}</label>
|
||
<input
|
||
type="number"
|
||
min="0"
|
||
value={formData[field.id]}
|
||
onChange={(e) => handleChange(field.id, 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>
|
||
|
||
{/* Images */}
|
||
<div className="bg-gray-50 p-4 rounded-xl">
|
||
<h3 className="text-lg font-bold text-gray-900 mb-4">{t("images")}</h3>
|
||
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-3">
|
||
{existingImages.map((path, i) => (
|
||
<div key={`e-${i}`} className="relative group aspect-square rounded-xl overflow-hidden bg-white border border-gray-200">
|
||
<img src={editImgUrl(path)} alt="" className="w-full h-full object-cover" />
|
||
<div className="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center gap-2">
|
||
<button
|
||
type="button"
|
||
onClick={() => handleRemoveExistingImage(i)}
|
||
className="w-8 h-8 bg-red-500 text-white rounded-full flex items-center justify-center hover:bg-red-600 shadow-md"
|
||
>
|
||
<Trash2 className="w-4 h-4" />
|
||
</button>
|
||
{i > 0 && (
|
||
<button
|
||
type="button"
|
||
onClick={() => handleMoveExistingImage(i, -1)}
|
||
className="w-8 h-8 bg-white text-gray-700 rounded-full flex items-center justify-center hover:bg-gray-100 shadow-md"
|
||
>
|
||
<ChevronLeft className="w-4 h-4" />
|
||
</button>
|
||
)}
|
||
{i < existingImages.length - 1 && (
|
||
<button
|
||
type="button"
|
||
onClick={() => handleMoveExistingImage(i, 1)}
|
||
className="w-8 h-8 bg-white text-gray-700 rounded-full flex items-center justify-center hover:bg-gray-100 shadow-md"
|
||
>
|
||
<ChevronRight className="w-4 h-4" />
|
||
</button>
|
||
)}
|
||
</div>
|
||
</div>
|
||
))}
|
||
{newImages.map((entry, i) => (
|
||
<div key={`n-${i}`} className="relative group aspect-square rounded-xl overflow-hidden bg-white border border-gray-200">
|
||
{entry.uploading ? (
|
||
<div className="w-full h-full flex items-center justify-center bg-gray-100">
|
||
<Loader2 className="w-8 h-8 text-amber-500 animate-spin" />
|
||
</div>
|
||
) : (
|
||
<img src={entry.preview} alt="" className="w-full h-full object-cover" />
|
||
)}
|
||
<div className="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center gap-2">
|
||
<button type="button" onClick={() => handleRemoveNewImage(i)} className="w-8 h-8 bg-red-500 text-white rounded-full flex items-center justify-center hover:bg-red-600 shadow-md">
|
||
<Trash2 className="w-4 h-4" />
|
||
</button>
|
||
{i > 0 && (
|
||
<button
|
||
type="button"
|
||
onClick={() => handleMoveNewImage(i, -1)}
|
||
className="w-8 h-8 bg-white text-gray-700 rounded-full flex items-center justify-center hover:bg-gray-100 shadow-md"
|
||
>
|
||
<ChevronLeft className="w-4 h-4" />
|
||
</button>
|
||
)}
|
||
{i < newImages.length - 1 && (
|
||
<button
|
||
type="button"
|
||
onClick={() => handleMoveNewImage(i, 1)}
|
||
className="w-8 h-8 bg-white text-gray-700 rounded-full flex items-center justify-center hover:bg-gray-100 shadow-md"
|
||
>
|
||
<ChevronRight className="w-4 h-4" />
|
||
</button>
|
||
)}
|
||
</div>
|
||
</div>
|
||
))}
|
||
{existingImages.length + newImages.length < 10 && (
|
||
<label className="aspect-square rounded-xl border-2 border-dashed border-gray-300 bg-white flex flex-col items-center justify-center gap-1 cursor-pointer hover:border-amber-300 hover:bg-amber-50 transition-all">
|
||
<ImageIcon className="w-8 h-8 text-gray-400" />
|
||
<span className="text-xs text-gray-500">{t("addImage")}</span>
|
||
<input
|
||
type="file"
|
||
accept="image/*"
|
||
multiple
|
||
className="hidden"
|
||
onChange={(e) => {
|
||
handleAddImages(e.target.files);
|
||
e.target.value = "";
|
||
}}
|
||
/>
|
||
</label>
|
||
)}
|
||
</div>
|
||
<p className="text-xs text-gray-400 mt-2">{existingImages.length + newImages.length}/10</p>
|
||
</div>
|
||
|
||
{/* Services */}
|
||
<div className="bg-gray-50 p-4 rounded-xl">
|
||
<h3 className="text-lg font-bold text-gray-900 mb-4">{t("services")}</h3>
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||
{Object.entries(serviceI18nKeys).map(([key, i18nKey]) => (
|
||
<div key={key} className="flex items-start gap-2 p-2 border rounded-lg bg-white">
|
||
<input type="checkbox" checked={formData.services?.[key] || false} onChange={(e) => handleServiceToggle(key, e.target.checked)} className="w-4 h-4 text-amber-500 mt-1" />
|
||
<div className="flex-1">
|
||
<span className="text-sm font-medium">{t(i18nKey)}</span>
|
||
{formData.services?.[key] && (
|
||
<input
|
||
type="text"
|
||
value={formData.serviceDetails?.[key] || ""}
|
||
onChange={(e) => handleServiceDetail(key, e.target.value)}
|
||
placeholder={t("serviceDetailPlaceholder")}
|
||
className="w-full mt-1 px-2 py-1 text-xs border border-gray-200 rounded focus:outline-none focus:ring-1 focus:ring-amber-500"
|
||
/>
|
||
)}
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Terms - rent only */}
|
||
{formData.purpose === "rent" && (
|
||
<div className="bg-gray-50 p-4 rounded-xl">
|
||
<h3 className="text-lg font-bold text-gray-900 mb-4">{t("termsOfUse")}</h3>
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||
{Object.entries(termI18nKeys).map(([key, i18nKey]) => (
|
||
<label key={key} className="flex items-center gap-2 p-2 border rounded-lg cursor-pointer bg-white hover:bg-amber-50">
|
||
<input type="checkbox" checked={formData.terms?.[key] || false} onChange={(e) => handleTermToggle(key, e.target.checked)} className="w-4 h-4 text-amber-500" />
|
||
<span className="text-sm">{t(i18nKey)}</span>
|
||
</label>
|
||
))}
|
||
</div>
|
||
<div className="mt-4">
|
||
<label className="block text-sm font-medium text-gray-700 mb-2">{t("customTerms")}</label>
|
||
<div className="flex gap-2 mb-2">
|
||
<input
|
||
type="text"
|
||
value={newCustomTerm}
|
||
onChange={(e) => setNewCustomTerm(e.target.value)}
|
||
onKeyDown={(e) => e.key === "Enter" && addCustomTerm()}
|
||
placeholder={t("addCustomTermPlaceholder")}
|
||
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-amber-500 text-sm"
|
||
/>
|
||
<button onClick={addCustomTerm} className="px-3 py-2 bg-amber-500 text-white rounded-lg text-sm hover:bg-amber-600">
|
||
{t("add")}
|
||
</button>
|
||
</div>
|
||
{formData.customTerms?.length > 0 && (
|
||
<div className="flex flex-wrap gap-2">
|
||
{formData.customTerms.map((term, i) => (
|
||
<span key={i} className="inline-flex items-center gap-1 px-3 py-1 bg-amber-100 text-amber-800 rounded-full text-sm">
|
||
{term}
|
||
<button onClick={() => removeCustomTerm(i)} className="hover:text-red-600">
|
||
<X className="w-3 h-3" />
|
||
</button>
|
||
</span>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Nearby Distances */}
|
||
<div className="bg-gray-50 p-4 rounded-xl">
|
||
<h3 className="text-lg font-bold text-gray-900 mb-4">{t("proximityToServices")}</h3>
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||
{[
|
||
{ id: "nearbySchool", label: t("school") },
|
||
{ id: "nearbyHospital", label: t("hospital") },
|
||
{ id: "nearbyRestaurant", label: t("restaurant") },
|
||
{ id: "nearbyUniversity", label: t("university") },
|
||
{ id: "nearbyPark", label: t("park") },
|
||
{ id: "nearbyMall", label: t("mall") },
|
||
].map((field) => (
|
||
<div key={field.id}>
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">{field.label}</label>
|
||
<input
|
||
type="text"
|
||
value={formData[field.id] || ""}
|
||
onChange={(e) => handleChange(field.id, e.target.value)}
|
||
placeholder={t("distancePlaceholder")}
|
||
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>
|
||
|
||
{/* Pricing */}
|
||
<div className="bg-gray-50 p-4 rounded-xl">
|
||
<h3 className="text-lg font-bold text-gray-900 mb-4">{formData.purpose === "rent" ? t("priceInfoRent") : t("salePriceSimple")}</h3>
|
||
{formData.purpose === "rent" ? (
|
||
<div className="space-y-4">
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">{t("dailyPrice")}</label>
|
||
<input
|
||
type="number"
|
||
min="0"
|
||
value={formData.dailyPrice}
|
||
onChange={(e) => handleChange("dailyPrice", 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>
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">{t("monthlyPrice")}</label>
|
||
<input
|
||
type="number"
|
||
min="0"
|
||
value={formData.monthlyPrice}
|
||
onChange={(e) => handleChange("monthlyPrice", 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("deposit")}</label>
|
||
<input
|
||
type="number"
|
||
min="0"
|
||
value={formData.deposit}
|
||
onChange={(e) => handleChange("deposit", 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>
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">{t("rentType")}</label>
|
||
<select
|
||
value={formData.rentType}
|
||
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"
|
||
>
|
||
<option value="daily">{t("daily")}</option>
|
||
<option value="monthly">{t("monthly")}</option>
|
||
<option value="both">{t("dailyAndMonthly")}</option>
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">{t("allowedPaymentPeriod")}</label>
|
||
<select
|
||
value={formData.allowedPaymentPeriod}
|
||
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"
|
||
>
|
||
<option value="">{t("selectPeriod")}</option>
|
||
<option value="1.00:00:00">{t("daily")}</option>
|
||
<option value="7.00:00:00">{t("weekly")}</option>
|
||
<option value="30.00:00:00">{t("monthly")}</option>
|
||
<option value="90.00:00:00">{t("quarterly")}</option>
|
||
<option value="365.00:00:00">{t("yearly")}</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
) : (
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">{t("salePriceSimple")}</label>
|
||
<input
|
||
type="number"
|
||
min="0"
|
||
value={formData.salePrice}
|
||
onChange={(e) => handleChange("salePrice", 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>
|
||
|
||
<div className="p-6 border-t flex gap-3">
|
||
<button onClick={onClose} className="flex-1 py-3 bg-gray-100 text-gray-700 rounded-xl hover:bg-gray-200 transition-colors">
|
||
{t("cancel")}
|
||
</button>
|
||
<button
|
||
onClick={handleSave}
|
||
disabled={isSaving}
|
||
className="flex-1 bg-gradient-to-r from-amber-500 to-amber-600 text-white py-3 rounded-xl hover:from-amber-600 hover:to-amber-700 transition-all disabled:opacity-50 flex items-center justify-center gap-2"
|
||
>
|
||
{isSaving ? (
|
||
<>
|
||
<Loader2 className="w-5 h-5 animate-spin" />
|
||
{t("saving")}
|
||
</>
|
||
) : (
|
||
<>
|
||
<Save className="w-5 h-5" />
|
||
{t("saveChanges")}
|
||
</>
|
||
)}
|
||
</button>
|
||
</div>
|
||
</motion.div>
|
||
</motion.div>
|
||
);
|
||
};
|
||
|
||
export default function OwnerPropertiesPage() {
|
||
const { t } = useTranslation();
|
||
const router = useRouter();
|
||
const [user, setUser] = useState(null);
|
||
const [properties, setProperties] = useState([]);
|
||
const [isLoading, setIsLoading] = useState(true);
|
||
const [showAddMenu, setShowAddMenu] = useState(false);
|
||
const [activeTab, setActiveTab] = useState("rent");
|
||
|
||
const [deleteModal, setDeleteModal] = useState({
|
||
isOpen: false,
|
||
property: null,
|
||
});
|
||
const [viewModal, setViewModal] = useState({ isOpen: false, property: null });
|
||
const [editModal, setEditModal] = useState({ isOpen: false, property: null });
|
||
const [deactivateModal, setDeactivateModal] = useState({
|
||
isOpen: false,
|
||
property: null,
|
||
isActivating: false,
|
||
});
|
||
|
||
const filteredProperties = properties.filter((p) => p.purpose === activeTab);
|
||
const rentCount = properties.filter((p) => p.purpose === "rent").length;
|
||
const saleCount = properties.filter((p) => p.purpose === "sale").length;
|
||
|
||
useEffect(() => {
|
||
const authUser = AuthService.getUser();
|
||
if (authUser && AuthService.isOwner()) {
|
||
setUser({
|
||
name: authUser.name || authUser.email,
|
||
email: authUser.email,
|
||
role: "owner",
|
||
});
|
||
loadProperties();
|
||
} else {
|
||
router.push("/auth/choose-role");
|
||
}
|
||
}, [router]);
|
||
|
||
const loadProperties = async () => {
|
||
const authUser = AuthService.getUser();
|
||
const userId = authUser?.id;
|
||
|
||
if (!userId) {
|
||
console.warn("[OwnerProperties] No user ID found");
|
||
setIsLoading(false);
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const [rentData, saleData] = await Promise.allSettled([getMyRentListings(), getMySaleListings()]);
|
||
|
||
const rentList = rentData.status === "fulfilled" ? (Array.isArray(rentData.value) ? rentData.value.filter(Boolean) : rentData.value ? [rentData.value] : []) : [];
|
||
const saleList = saleData.status === "fulfilled" ? (Array.isArray(saleData.value) ? saleData.value.filter(Boolean) : saleData.value ? [saleData.value] : []) : [];
|
||
|
||
const normalizeServices = (details) => {
|
||
const rawServices = details.services || {};
|
||
const serviceList = Array.isArray(rawServices) ? rawServices : Object.keys(rawServices).filter((k) => rawServices[k]);
|
||
const serviceDetails = details.serviceDetails || {};
|
||
const services = {};
|
||
serviceList.forEach((s) => {
|
||
services[s] = serviceDetails[s] || true;
|
||
});
|
||
return services;
|
||
};
|
||
|
||
const normalizeTerms = (terms) => {
|
||
if (!terms) return {};
|
||
return Array.isArray(terms) ? terms.reduce((acc, t) => ({ ...acc, [t]: true }), {}) : terms;
|
||
};
|
||
|
||
const normalizeProximity = (prox) => {
|
||
if (!prox) return {};
|
||
const result = {};
|
||
Object.entries(prox).forEach(([k, v]) => {
|
||
if (!v) return;
|
||
result[k.charAt(0).toUpperCase() + k.slice(1)] = v;
|
||
});
|
||
return result;
|
||
};
|
||
|
||
const mappedRent = rentList.map((item) => {
|
||
const info = item.propertyInformation || {};
|
||
const details =
|
||
typeof info.detailsJSON === "object" && info.detailsJSON
|
||
? info.detailsJSON
|
||
: (() => {
|
||
try {
|
||
return JSON.parse(info.detailsJSON || "{}");
|
||
} catch {
|
||
return {};
|
||
}
|
||
})();
|
||
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 : [];
|
||
return {
|
||
id: item.id,
|
||
title: info.address || `عقار #${item.id}`,
|
||
propertyType:
|
||
{
|
||
0: "apartment",
|
||
1: "villa",
|
||
2: "sweet",
|
||
3: "room",
|
||
4: "studio",
|
||
5: "office",
|
||
6: "farms",
|
||
7: "shop",
|
||
8: "warehouse",
|
||
}[info.buildingType] || "apartment",
|
||
propertyTypeLabel:
|
||
{
|
||
0: "شقة",
|
||
1: "فيلا",
|
||
2: "سويت",
|
||
3: "غرفة",
|
||
4: "استوديو",
|
||
5: "مكتب",
|
||
6: "مزرعة",
|
||
7: "متجر",
|
||
8: "مستودع",
|
||
}[info.buildingType] || "عقار",
|
||
purpose: "rent",
|
||
rentType: item.rentType === 0 ? "monthly" : item.rentType === 1 ? "daily" : "daily",
|
||
dailyPrice: item.dailyRent || 0,
|
||
monthlyPrice: item.monthlyRent || 0,
|
||
salePrice: item.price || 0,
|
||
deposit: item.deposit || 0,
|
||
location: info.address || "",
|
||
bedrooms: info.numberOfBedRooms || 0,
|
||
bathrooms: info.numberOfBathRooms || 0,
|
||
area: info.space || 0,
|
||
livingRooms: details.livingRooms || 0,
|
||
floor: details.floorNumber ?? details.floor ?? 0,
|
||
salons: details.numberOfSalons ?? details.salonsCount ?? details.salons ?? 0,
|
||
balconies: details.numberOfBalconies ?? details.balconiesCount ?? details.balconies ?? 0,
|
||
bookedCount: details.bookedCount || 0,
|
||
status: { 0: "available", 1: "booked", 2: "maintenance" }[info.status] || "available",
|
||
images: raw.length > 0 ? raw.map((img) => (img.startsWith("http") ? img : `${apiBase}${img.startsWith("/") ? "" : "/Pictures/"}${img}`)) : ["/property-placeholder.jpg"],
|
||
createdAt: item.createdAt || new Date().toISOString(),
|
||
furnished: details.furnished || false,
|
||
displayType: details.displayType || (item.dailyRent && item.monthlyRent ? "Both" : item.monthlyRent ? "Monthly rent" : item.dailyRent ? "Daily rent" : "Both"),
|
||
description: info.description || "",
|
||
address: info.address || "",
|
||
city: "",
|
||
district: "",
|
||
services: normalizeServices(details),
|
||
terms: normalizeTerms(details.terms),
|
||
proximity: normalizeProximity(details.nearbyDistances),
|
||
rating: item.rating || 0,
|
||
currencyId: item.currencyId,
|
||
_raw: item,
|
||
};
|
||
});
|
||
|
||
const mappedSale = saleList.map((item) => {
|
||
const info = item.propertyInformation || {};
|
||
const details =
|
||
typeof info.detailsJSON === "object" && info.detailsJSON
|
||
? info.detailsJSON
|
||
: (() => {
|
||
try {
|
||
return JSON.parse(info.detailsJSON || "{}");
|
||
} catch {
|
||
return {};
|
||
}
|
||
})();
|
||
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 : [];
|
||
return {
|
||
id: item.id,
|
||
title: info.address || `عقار للبيع #${item.id}`,
|
||
propertyType:
|
||
{
|
||
0: "apartment",
|
||
1: "villa",
|
||
2: "sweet",
|
||
3: "room",
|
||
4: "studio",
|
||
5: "office",
|
||
6: "farms",
|
||
7: "shop",
|
||
8: "warehouse",
|
||
}[info.buildingType] || "apartment",
|
||
propertyTypeLabel:
|
||
{
|
||
0: "شقة",
|
||
1: "فيلا",
|
||
2: "سويت",
|
||
3: "غرفة",
|
||
4: "استوديو",
|
||
5: "مكتب",
|
||
6: "مزرعة",
|
||
7: "متجر",
|
||
8: "مستودع",
|
||
}[info.buildingType] || "عقار",
|
||
purpose: "sale",
|
||
dailyPrice: 0,
|
||
monthlyPrice: 0,
|
||
salePrice: item.price || 0,
|
||
deposit: 0,
|
||
location: info.address || "",
|
||
bedrooms: info.numberOfBedRooms || 0,
|
||
bathrooms: info.numberOfBathRooms || 0,
|
||
area: info.space || 0,
|
||
livingRooms: details.livingRooms || 0,
|
||
floor: details.floorNumber ?? details.floor ?? 0,
|
||
salons: details.numberOfSalons ?? details.salonsCount ?? details.salons ?? 0,
|
||
balconies: details.numberOfBalconies ?? details.balconiesCount ?? details.balconies ?? 0,
|
||
bookedCount: details.bookedCount || 0,
|
||
status: "available",
|
||
images: raw.length > 0 ? raw.map((img) => (img.startsWith("http") ? img : `${apiBase}${img.startsWith("/") ? "" : "/Pictures/"}${img}`)) : ["/property-placeholder.jpg"],
|
||
createdAt: item.createdAt || new Date().toISOString(),
|
||
furnished: details.furnished || false,
|
||
displayType: details.displayType || "For sale",
|
||
description: info.description || "",
|
||
address: info.address || "",
|
||
city: "",
|
||
district: "",
|
||
services: normalizeServices(details),
|
||
terms: normalizeTerms(details.terms),
|
||
proximity: normalizeProximity(details.nearbyDistances),
|
||
rating: item.rating || 0,
|
||
currencyId: item.currencyId,
|
||
_raw: item,
|
||
};
|
||
});
|
||
|
||
setProperties([...mappedRent, ...mappedSale]);
|
||
} catch (err) {
|
||
console.error("[OwnerProperties] Failed to load properties:", err);
|
||
toast.error(t("loadFailed"));
|
||
} finally {
|
||
setIsLoading(false);
|
||
}
|
||
};
|
||
|
||
const updatePropertiesInStorage = (newProperties) => {
|
||
setProperties(newProperties);
|
||
localStorage.setItem("ownerProperties", JSON.stringify(newProperties));
|
||
};
|
||
|
||
const handleDelete = () => {
|
||
if (deleteModal.property) {
|
||
const newProperties = properties.filter((p) => p.id !== deleteModal.property.id);
|
||
updatePropertiesInStorage(newProperties);
|
||
setDeleteModal({ isOpen: false, property: null });
|
||
toast.success(t("deleteSuccess"));
|
||
}
|
||
};
|
||
|
||
const handleSaveEdit = async (formData) => {
|
||
try {
|
||
const property = editModal.property;
|
||
const raw = property._raw || {};
|
||
const rawInfo = raw.propertyInformation || {};
|
||
|
||
const buildingTypeMap = {
|
||
apartment: 0,
|
||
villa: 1,
|
||
sweet: 2,
|
||
room: 3,
|
||
studio: 4,
|
||
office: 5,
|
||
farms: 6,
|
||
shop: 7,
|
||
warehouse: 8,
|
||
};
|
||
|
||
const activeServices = Object.entries(formData.services || {})
|
||
.filter(([, v]) => v)
|
||
.map(([k]) => k);
|
||
|
||
const activeTerms = Object.entries(formData.terms || {})
|
||
.filter(([, v]) => v)
|
||
.reduce((acc, [k]) => ({ ...acc, [k]: true }), {});
|
||
|
||
if (formData.customTerms?.length) {
|
||
formData.customTerms.forEach((t) => {
|
||
activeTerms[t] = true;
|
||
});
|
||
}
|
||
|
||
const details = {
|
||
description: formData.description || "",
|
||
services: activeServices,
|
||
serviceDetails: Object.fromEntries(Object.entries(formData.serviceDetails || {}).filter(([k, v]) => activeServices.includes(k) && v)),
|
||
...(formData.purpose === "rent" ? { terms: activeTerms } : {}),
|
||
displayType: formData.purpose === "rent" ? (formData.rentType === "both" ? "Both" : formData.rentType === "daily" ? "Daily rent" : "Monthly rent") : "For sale",
|
||
propertyCondition: formData.furnished ? "WithFurniture" : "WithoutFurniture",
|
||
floorNumber: parseInt(formData.floor) || 0,
|
||
numberOfSalons: parseInt(formData.salons) || 0,
|
||
numberOfBalconies: parseInt(formData.balconies) || 0,
|
||
numberOfLivingRooms: parseInt(formData.livingRooms) || 0,
|
||
nearbyDistances: {
|
||
school: formData.nearbySchool || "",
|
||
hospital: formData.nearbyHospital || "",
|
||
restaurant: formData.nearbyRestaurant || "",
|
||
university: formData.nearbyUniversity || "",
|
||
park: formData.nearbyPark || "",
|
||
mall: formData.nearbyMall || "",
|
||
},
|
||
};
|
||
|
||
if (formData.purpose === "rent" && formData.propertyType === "room") {
|
||
const roomDetails = rawInfo.detailsJSON || {};
|
||
let parsedDetails = {};
|
||
try {
|
||
parsedDetails = typeof roomDetails === "object" ? roomDetails : JSON.parse(roomDetails);
|
||
} catch {
|
||
parsedDetails = {};
|
||
}
|
||
details.room = parsedDetails.room || {};
|
||
}
|
||
|
||
const detailsJSON = JSON.stringify(details);
|
||
|
||
const propInfo = {
|
||
cordsX: rawInfo.cordsX || "",
|
||
cordsY: rawInfo.cordsY || "",
|
||
images: formData._images || rawInfo.images || [],
|
||
address: property.address || rawInfo.address || "",
|
||
description: formData.description || rawInfo.description || "",
|
||
numberOfBathRooms: parseInt(formData.bathrooms) || 0,
|
||
numberOfRooms: parseInt(formData.bedrooms) || 0,
|
||
numberOfBedRooms: parseInt(formData.bedrooms) || 0,
|
||
space: parseFloat(formData.area) || 0,
|
||
detailsJSON,
|
||
buildingType: buildingTypeMap[formData.propertyType] ?? 0,
|
||
status: 0,
|
||
propertyType: formData.furnished ? 0 : 1,
|
||
};
|
||
|
||
if (formData.purpose === "rent") {
|
||
const rentTypeMap = { daily: 1, monthly: 0, both: 0 };
|
||
const payload = buildRentPropertyPayload({
|
||
propertyInformation: propInfo,
|
||
city: formData.city || property?.city || property?.governorate || "",
|
||
governorate: formData.city || property?.city || property?.governorate || "",
|
||
documentType: formData.documentType || property?.documentType || "",
|
||
deposit: parseFloat(formData.deposit) || 0,
|
||
monthlyRent: parseFloat(formData.monthlyPrice) || 0,
|
||
dailyRent: parseFloat(formData.dailyPrice) || 0,
|
||
rating: 1,
|
||
currencyId: formData.currencyId || property.currencyId || 1,
|
||
rentType: rentTypeMap[formData.rentType] ?? 0,
|
||
type: formData.furnished ? 0 : 1,
|
||
allowedPaymentPeriod: formData.allowedPaymentPeriod || "1.00:00:00",
|
||
});
|
||
await editRentProperty(property.id, payload);
|
||
} else {
|
||
const payload = {
|
||
propInfo,
|
||
price: parseFloat(formData.salePrice) || 0,
|
||
currencyId: formData.currencyId || property.currencyId || 1,
|
||
};
|
||
await editSaleProperty(property.id, payload);
|
||
}
|
||
|
||
const updatedProperty = { ...property, ...formData };
|
||
const newProperties = properties.map((p) => (p.id === property.id ? updatedProperty : p));
|
||
updatePropertiesInStorage(newProperties);
|
||
setEditModal({ isOpen: false, property: null });
|
||
toast.success(t("updateSuccess"));
|
||
} catch (err) {
|
||
console.error("[OwnerProperties] Edit failed:", err);
|
||
toast.error(t("updateFailed"));
|
||
throw err;
|
||
}
|
||
};
|
||
|
||
const handleToggleActivation = async () => {
|
||
const prop = deactivateModal.property;
|
||
if (!prop) return;
|
||
|
||
const willActivate = deactivateModal.isActivating;
|
||
const newStatus = willActivate ? "available" : "notAvailable";
|
||
const statusCode = willActivate ? 0 : 1;
|
||
|
||
setDeactivateModal({ isOpen: false, property: null, isActivating: false });
|
||
|
||
try {
|
||
if (prop.purpose === "rent") {
|
||
await updateRentPropertyStatus(prop.id, statusCode);
|
||
} else {
|
||
await updateSalePropertyStatus(prop.id, statusCode);
|
||
}
|
||
|
||
const newProperties = properties.map((p) => (p.id === prop.id ? { ...p, status: newStatus } : p));
|
||
setProperties(newProperties);
|
||
localStorage.setItem("ownerProperties", JSON.stringify(newProperties));
|
||
toast.success(willActivate ? t("activateSuccess") : t("deactivateSuccess"));
|
||
} catch (err) {
|
||
console.error("[OwnerProperties] Toggle status failed:", err);
|
||
toast.error(t("statusUpdateFailed"));
|
||
}
|
||
};
|
||
|
||
const fadeInUp = {
|
||
initial: { opacity: 0, y: 20 },
|
||
animate: { opacity: 1, y: 0 },
|
||
transition: { duration: 0.5 },
|
||
};
|
||
|
||
if (isLoading) {
|
||
return (
|
||
<Loading/>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div className="min-h-screen bg-gray-50 py-8 relative">
|
||
<Toaster position="top-center" reverseOrder={false} />
|
||
|
||
<DeleteConfirmationModal isOpen={deleteModal.isOpen} onClose={() => setDeleteModal({ isOpen: false, property: null })} onConfirm={handleDelete} propertyTitle={deleteModal.property?.title} />
|
||
|
||
<PropertyViewModal isOpen={viewModal.isOpen} onClose={() => setViewModal({ isOpen: false, property: null })} property={viewModal.property} />
|
||
|
||
<PropertyEditModal isOpen={editModal.isOpen} onClose={() => setEditModal({ isOpen: false, property: null })} property={editModal.property} onSave={handleSaveEdit} />
|
||
|
||
<DeactivateConfirmationModal
|
||
isOpen={deactivateModal.isOpen}
|
||
onClose={() => setDeactivateModal({ isOpen: false, property: null, isActivating: false })}
|
||
onConfirm={handleToggleActivation}
|
||
propertyTitle={deactivateModal.property?.title}
|
||
isActivating={deactivateModal.isActivating}
|
||
/>
|
||
|
||
<div className="container mx-auto px-4">
|
||
<motion.div initial={{ opacity: 0, y: -20 }} animate={{ opacity: 1, y: 0 }} className="flex justify-between items-center mb-8">
|
||
<div>
|
||
<h1 className="text-3xl font-bold text-gray-900 mb-2">{t("myProperties")}</h1>
|
||
<p className="text-gray-600">{t("welcomeUser", { name: user?.name, count: properties.length })}</p>
|
||
</div>
|
||
|
||
<div className="relative">
|
||
<motion.button
|
||
onClick={() => setShowAddMenu(!showAddMenu)}
|
||
className="bg-gradient-to-r from-amber-500 to-amber-600 text-white px-6 py-3 rounded-xl font-medium flex items-center gap-2 shadow-lg hover:shadow-xl transition-all"
|
||
whileHover={{ scale: 1.05 }}
|
||
whileTap={{ scale: 0.95 }}
|
||
>
|
||
<PlusCircle className="w-5 h-5" />
|
||
{t("addNewProperty")}
|
||
</motion.button>
|
||
|
||
<AnimatePresence>
|
||
{showAddMenu && (
|
||
<motion.div
|
||
initial={{ opacity: 0, y: -10 }}
|
||
animate={{ opacity: 1, y: 0 }}
|
||
exit={{ opacity: 0, y: -10 }}
|
||
className="absolute left-0 mt-2 w-64 bg-white rounded-xl shadow-xl border border-gray-200 overflow-hidden z-50"
|
||
>
|
||
<div className="p-2">
|
||
<Link
|
||
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"
|
||
onClick={() => setShowAddMenu(false)}
|
||
>
|
||
<div className="w-8 h-8 bg-green-100 rounded-lg flex items-center justify-center">
|
||
<Home className="w-4 h-4 text-green-600" />
|
||
</div>
|
||
<div>
|
||
<p className="font-medium">{t("rentProperty")}</p>
|
||
<p className="text-xs text-gray-500">{t("rentPropertyDesc")}</p>
|
||
</div>
|
||
</Link>
|
||
|
||
<Link
|
||
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"
|
||
onClick={() => setShowAddMenu(false)}
|
||
>
|
||
<div className="w-8 h-8 bg-blue-100 rounded-lg flex items-center justify-center">
|
||
<DollarSign className="w-4 h-4 text-blue-600" />
|
||
</div>
|
||
<div>
|
||
<p className="font-medium">{t("saleProperty")}</p>
|
||
<p className="text-xs text-gray-500">{t("salePropertyDesc")}</p>
|
||
</div>
|
||
</Link>
|
||
</div>
|
||
</motion.div>
|
||
)}
|
||
</AnimatePresence>
|
||
</div>
|
||
</motion.div>
|
||
|
||
{/* Tab Switcher */}
|
||
<div className="flex gap-1 mb-6 bg-gray-100 rounded-xl p-1 w-fit">
|
||
<button
|
||
onClick={() => setActiveTab("rent")}
|
||
className={`px-6 py-2 rounded-lg text-sm font-medium transition-all ${activeTab === "rent" ? "bg-white text-amber-600 shadow-sm" : "text-gray-600 hover:text-gray-800"}`}
|
||
>
|
||
<span className="flex items-center gap-2">
|
||
<Home className="w-4 h-4" />
|
||
{t("forRent")}
|
||
{rentCount > 0 && <span className="bg-amber-100 text-amber-800 text-xs px-2 py-0.5 rounded-full">{rentCount}</span>}
|
||
</span>
|
||
</button>
|
||
<button
|
||
onClick={() => setActiveTab("sale")}
|
||
className={`px-6 py-2 rounded-lg text-sm font-medium transition-all ${activeTab === "sale" ? "bg-white text-blue-600 shadow-sm" : "text-gray-600 hover:text-gray-800"}`}
|
||
>
|
||
<span className="flex items-center gap-2">
|
||
<DollarSign className="w-4 h-4" />
|
||
{t("forSaleTab")}
|
||
{saleCount > 0 && <span className="bg-blue-100 text-blue-800 text-xs px-2 py-0.5 rounded-full">{saleCount}</span>}
|
||
</span>
|
||
</button>
|
||
</div>
|
||
|
||
{filteredProperties.length === 0 ? (
|
||
<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} className="bg-white rounded-2xl p-12 text-center border-2 border-dashed border-gray-300">
|
||
<div className="w-24 h-24 bg-amber-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
||
<Building className="w-12 h-12 text-amber-600" />
|
||
</div>
|
||
<h3 className="text-xl font-bold text-gray-900 mb-2">{activeTab === "rent" ? t("noRentProperties") : t("noSaleProperties")}</h3>
|
||
<p className="text-gray-600 mb-6">{t("startAdding", { type: activeTab === "rent" ? t("forRent") : t("forSaleTab") })}</p>
|
||
<Link
|
||
href={`/owner/properties/add?purpose=${activeTab}`}
|
||
className="inline-flex items-center gap-2 bg-amber-500 text-white px-6 py-3 rounded-xl font-medium hover:bg-amber-600 transition-colors"
|
||
>
|
||
<PlusCircle className="w-5 h-5" />
|
||
{t("addNewProperty")}
|
||
</Link>
|
||
</motion.div>
|
||
) : (
|
||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||
{filteredProperties.map((property, index) => (
|
||
<motion.div
|
||
key={property.id}
|
||
variants={fadeInUp}
|
||
initial="initial"
|
||
animate="animate"
|
||
transition={{ delay: index * 0.1 }}
|
||
className="bg-white rounded-2xl shadow-sm hover:shadow-md transition-all overflow-hidden border border-gray-200"
|
||
>
|
||
<div className="relative aspect-[16/10] bg-gray-100 overflow-hidden">
|
||
{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>
|
||
)}
|
||
<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 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>
|
||
</motion.div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|