Files
SweetHome/app/owner/properties/page.js
mouazkh b4bb3ec279
All checks were successful
Build frontend / build (push) Successful in 1m10s
fixed some changes in the ccode
2026-07-25 13:26:48 +03:00

2109 lines
82 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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,
} from "../../utils/api";
import { PropertyService } from "../../enums/PropertyService";
import { PropertyTerm } from "../../enums/PropertyTerm";
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);
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('');
}, [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 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 {
await onSave(formData);
} 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>
{/* 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: 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 (
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
<div className="text-center">
<Loader2 className="w-12 h-12 text-amber-500 animate-spin mx-auto mb-4" />
<p className="text-gray-600">{t('loadingProperties')}</p>
</div>
</div>
);
}
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-amber-50 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-amber-50 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>
);
}