2026-03-17 20:36:59 +03:00
|
|
|
'use client';
|
|
|
|
|
|
|
|
|
|
import { useState, useRef, useEffect } from 'react';
|
|
|
|
|
import { motion, AnimatePresence } from 'framer-motion';
|
|
|
|
|
import { useRouter } from 'next/navigation';
|
|
|
|
|
import Link from 'next/link';
|
|
|
|
|
import Image from 'next/image';
|
|
|
|
|
import dynamic from 'next/dynamic';
|
|
|
|
|
import 'leaflet/dist/leaflet.css';
|
|
|
|
|
import {
|
|
|
|
|
ArrowLeft,
|
|
|
|
|
MapPin,
|
|
|
|
|
Camera,
|
|
|
|
|
X,
|
|
|
|
|
Home,
|
|
|
|
|
Building,
|
|
|
|
|
Bed,
|
|
|
|
|
Bath,
|
|
|
|
|
Square,
|
|
|
|
|
DollarSign,
|
|
|
|
|
Calendar,
|
|
|
|
|
Clock,
|
|
|
|
|
CheckCircle,
|
|
|
|
|
AlertCircle,
|
|
|
|
|
Info,
|
|
|
|
|
ChevronRight,
|
|
|
|
|
ChevronLeft,
|
|
|
|
|
Loader2,
|
|
|
|
|
Upload,
|
|
|
|
|
FileText,
|
|
|
|
|
Shield,
|
|
|
|
|
HelpCircle,
|
|
|
|
|
Search,
|
|
|
|
|
Navigation,
|
|
|
|
|
Wifi,
|
|
|
|
|
Zap,
|
|
|
|
|
Flame,
|
|
|
|
|
Droplets,
|
|
|
|
|
Cigarette,
|
|
|
|
|
Dog,
|
|
|
|
|
Music,
|
|
|
|
|
Star,
|
|
|
|
|
Sofa,
|
|
|
|
|
DoorOpen,
|
|
|
|
|
Warehouse,
|
|
|
|
|
Layers,
|
|
|
|
|
Plus,
|
|
|
|
|
Minus,
|
|
|
|
|
Save,
|
|
|
|
|
Wind,
|
|
|
|
|
Move
|
|
|
|
|
} from 'lucide-react';
|
|
|
|
|
import toast, { Toaster } from 'react-hot-toast';
|
2026-03-29 15:48:48 +00:00
|
|
|
import { addRentProperty } from '../../../utils/api';
|
|
|
|
|
import {
|
|
|
|
|
BuildingType,
|
|
|
|
|
RentPropertyCondition,
|
|
|
|
|
RentPropertyType,
|
|
|
|
|
RentType,
|
|
|
|
|
PropertyService,
|
|
|
|
|
PropertyServiceLabels,
|
|
|
|
|
PropertyServicesList,
|
|
|
|
|
PropertyTerm,
|
|
|
|
|
PropertyTermLabels,
|
|
|
|
|
PropertyTermsList,
|
|
|
|
|
Currency,
|
|
|
|
|
CurrencyLabels
|
|
|
|
|
} from '../../../enums';
|
2026-03-17 20:36:59 +03:00
|
|
|
|
|
|
|
|
const MapContainer = dynamic(() => import('react-leaflet').then(mod => mod.MapContainer), { ssr: false });
|
|
|
|
|
const TileLayer = dynamic(() => import('react-leaflet').then(mod => mod.TileLayer), { ssr: false });
|
|
|
|
|
const Marker = dynamic(() => import('react-leaflet').then(mod => mod.Marker), { ssr: false });
|
|
|
|
|
const Popup = dynamic(() => import('react-leaflet').then(mod => mod.Popup), { ssr: false });
|
|
|
|
|
const useMapEvents = dynamic(() => import('react-leaflet').then(mod => mod.useMapEvents), { ssr: false });
|
|
|
|
|
|
|
|
|
|
function MapClickHandler({ onMapClick }) {
|
|
|
|
|
const map = useMapEvents({
|
|
|
|
|
click: (e) => {
|
|
|
|
|
const { lat, lng } = e.latlng;
|
|
|
|
|
onMapClick([lat, lng]);
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default function AddPropertyPage() {
|
|
|
|
|
const router = useRouter();
|
|
|
|
|
|
|
|
|
|
const [step, setStep] = useState(1);
|
|
|
|
|
const totalSteps = 4;
|
|
|
|
|
|
|
|
|
|
const [formData, setFormData] = useState({
|
|
|
|
|
propertyType: 'apartment', // apartment, villa, suite, room
|
|
|
|
|
|
|
|
|
|
furnished: false,
|
|
|
|
|
|
|
|
|
|
bedrooms: 1,
|
|
|
|
|
bathrooms: 1,
|
|
|
|
|
livingRooms: 1,
|
|
|
|
|
|
|
|
|
|
services: {
|
2026-03-30 01:01:42 +00:00
|
|
|
[PropertyService.ELECTRICITY]: false,
|
|
|
|
|
[PropertyService.INTERNET]: false,
|
|
|
|
|
[PropertyService.HEATING]: false,
|
|
|
|
|
[PropertyService.WATER]: false,
|
|
|
|
|
[PropertyService.CENTRAL_AIR_CONDITIONING]: false,
|
|
|
|
|
[PropertyService.PARKING]: false,
|
|
|
|
|
[PropertyService.ELEVATOR]: false
|
2026-03-17 20:36:59 +03:00
|
|
|
},
|
|
|
|
|
|
2026-03-30 01:01:42 +00:00
|
|
|
serviceDetails: {},
|
|
|
|
|
|
2026-03-17 20:36:59 +03:00
|
|
|
terms: {
|
2026-03-30 01:01:42 +00:00
|
|
|
[PropertyTerm.NO_SMOKING]: false,
|
|
|
|
|
[PropertyTerm.NO_ANIMALS]: false,
|
|
|
|
|
[PropertyTerm.NO_PARTIES]: false
|
2026-03-17 20:36:59 +03:00
|
|
|
},
|
|
|
|
|
|
|
|
|
|
offerType: 'daily',
|
|
|
|
|
|
|
|
|
|
dailyPrice: '',
|
|
|
|
|
monthlyPrice: '',
|
|
|
|
|
|
|
|
|
|
city: '',
|
|
|
|
|
district: '',
|
|
|
|
|
address: '',
|
|
|
|
|
lat: null,
|
|
|
|
|
lng: null,
|
|
|
|
|
|
|
|
|
|
description: '',
|
|
|
|
|
|
|
|
|
|
images: []
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const [imagePreviews, setImagePreviews] = useState([]);
|
2026-03-30 01:06:23 +00:00
|
|
|
const [uploadedImagePaths, setUploadedImagePaths] = useState([]);
|
2026-03-17 20:36:59 +03:00
|
|
|
|
|
|
|
|
const [selectedLocation, setSelectedLocation] = useState(null);
|
|
|
|
|
const [mapCenter, setMapCenter] = useState([33.5138, 36.2765]);
|
2026-03-28 18:12:41 +00:00
|
|
|
const [mapZoom, setMapZoom] = useState(13);
|
2026-03-17 20:36:59 +03:00
|
|
|
const [searchQuery, setSearchQuery] = useState('');
|
|
|
|
|
const [mapLoaded, setMapLoaded] = useState(false);
|
2026-03-29 15:48:48 +00:00
|
|
|
const [selectedCurrencyId, setSelectedCurrencyId] = useState(Currency.SYP);
|
2026-03-17 20:36:59 +03:00
|
|
|
|
|
|
|
|
const [errors, setErrors] = useState({});
|
|
|
|
|
|
|
|
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
|
|
|
|
|
|
|
|
const fileInputRef = useRef(null);
|
|
|
|
|
|
|
|
|
|
const propertyTypes = [
|
|
|
|
|
{ id: 'apartment', label: 'شقة', icon: Building },
|
|
|
|
|
{ id: 'villa', label: 'فيلا', icon: Home },
|
|
|
|
|
{ id: 'suite', label: 'سويت', icon: Sofa },
|
|
|
|
|
{ id: 'room', label: 'غرفة ضمن شقة', icon: DoorOpen }
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const serviceList = [
|
2026-03-29 15:48:48 +00:00
|
|
|
{ id: PropertyService.ELECTRICITY, label: PropertyServiceLabels[PropertyService.ELECTRICITY], icon: Zap },
|
|
|
|
|
{ id: PropertyService.INTERNET, label: PropertyServiceLabels[PropertyService.INTERNET], icon: Wifi },
|
|
|
|
|
{ id: PropertyService.HEATING, label: PropertyServiceLabels[PropertyService.HEATING], icon: Flame },
|
|
|
|
|
{ id: PropertyService.WATER, label: PropertyServiceLabels[PropertyService.WATER], icon: Droplets },
|
|
|
|
|
{ id: PropertyService.CENTRAL_AIR_CONDITIONING, label: PropertyServiceLabels[PropertyService.CENTRAL_AIR_CONDITIONING], icon: Wind },
|
|
|
|
|
{ id: PropertyService.PARKING, label: PropertyServiceLabels[PropertyService.PARKING], icon: Warehouse },
|
|
|
|
|
{ id: PropertyService.ELEVATOR, label: PropertyServiceLabels[PropertyService.ELEVATOR], icon: Layers },
|
2026-03-17 20:36:59 +03:00
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const termsList = [
|
2026-03-29 15:48:48 +00:00
|
|
|
{ id: PropertyTerm.NO_SMOKING, label: PropertyTermLabels[PropertyTerm.NO_SMOKING], icon: Cigarette },
|
|
|
|
|
{ id: PropertyTerm.NO_ANIMALS, label: PropertyTermLabels[PropertyTerm.NO_ANIMALS], icon: Dog },
|
|
|
|
|
{ id: PropertyTerm.NO_PARTIES, label: PropertyTermLabels[PropertyTerm.NO_PARTIES], icon: Music },
|
2026-03-17 20:36:59 +03:00
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const offerTypes = [
|
|
|
|
|
{ id: 'daily', label: 'إيجار يومي', icon: Clock },
|
|
|
|
|
{ id: 'monthly', label: 'إيجار شهري', icon: Calendar },
|
|
|
|
|
{ id: 'both', label: 'إيجار يومي وشهري', icon: Calendar },
|
2026-03-30 00:57:52 +00:00
|
|
|
].filter(Boolean);
|
2026-03-17 20:36:59 +03:00
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (typeof window !== 'undefined') {
|
|
|
|
|
const L = require('leaflet');
|
|
|
|
|
delete L.Icon.Default.prototype._getIconUrl;
|
|
|
|
|
L.Icon.Default.mergeOptions({
|
|
|
|
|
iconRetinaUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon-2x.png',
|
|
|
|
|
iconUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon.png',
|
|
|
|
|
shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-shadow.png',
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
setMapLoaded(true);
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
const handleSearch = async () => {
|
|
|
|
|
if (!searchQuery) return;
|
|
|
|
|
|
|
|
|
|
toast.loading('جاري البحث...', { id: 'search' });
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const response = await fetch(
|
|
|
|
|
`https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(searchQuery)}&limit=1&accept-language=ar`
|
|
|
|
|
);
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
|
|
|
|
if (data && data.length > 0) {
|
|
|
|
|
const result = data[0];
|
|
|
|
|
const lat = parseFloat(result.lat);
|
|
|
|
|
const lng = parseFloat(result.lon);
|
|
|
|
|
|
|
|
|
|
setMapCenter([lat, lng]);
|
|
|
|
|
setMapZoom(18);
|
|
|
|
|
|
|
|
|
|
const addressResponse = await fetch(
|
|
|
|
|
`https://nominatim.openstreetmap.org/reverse?format=json&lat=${lat}&lon=${lng}&accept-language=ar`
|
|
|
|
|
);
|
|
|
|
|
const addressData = await addressResponse.json();
|
|
|
|
|
|
|
|
|
|
setSelectedLocation({
|
|
|
|
|
lat: lat,
|
|
|
|
|
lng: lng,
|
|
|
|
|
address: addressData.display_name || result.display_name
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
toast.success('تم العثور على الموقع', { id: 'search' });
|
|
|
|
|
} else {
|
|
|
|
|
toast.error('لم يتم العثور على العنوان', { id: 'search' });
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('خطأ في البحث:', error);
|
|
|
|
|
toast.error('حدث خطأ في البحث', { id: 'search' });
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleGeolocation = () => {
|
|
|
|
|
if (!navigator.geolocation) {
|
|
|
|
|
toast.error('المتصفح لا يدعم تحديد الموقع');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
toast.loading('جاري تحديد موقعك...', { id: 'geolocation' });
|
|
|
|
|
|
|
|
|
|
navigator.geolocation.getCurrentPosition(
|
|
|
|
|
async (position) => {
|
|
|
|
|
const { latitude, longitude } = position.coords;
|
|
|
|
|
setMapCenter([latitude, longitude]);
|
|
|
|
|
setMapZoom(18);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const response = await fetch(
|
|
|
|
|
`https://nominatim.openstreetmap.org/reverse?format=json&lat=${latitude}&lon=${longitude}&accept-language=ar`
|
|
|
|
|
);
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
|
|
|
|
setSelectedLocation({
|
|
|
|
|
lat: latitude,
|
|
|
|
|
lng: longitude,
|
|
|
|
|
address: data.display_name || 'موقعك الحالي'
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
toast.success('تم تحديد موقعك', { id: 'geolocation' });
|
|
|
|
|
} catch (error) {
|
|
|
|
|
setSelectedLocation({
|
|
|
|
|
lat: latitude,
|
|
|
|
|
lng: longitude,
|
|
|
|
|
address: 'موقعك الحالي'
|
|
|
|
|
});
|
|
|
|
|
toast.success('تم تحديد موقعك', { id: 'geolocation' });
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
(error) => {
|
|
|
|
|
toast.error('فشل في تحديد الموقع', { id: 'geolocation' });
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleMapClick = async (coords) => {
|
|
|
|
|
try {
|
|
|
|
|
const [lat, lng] = coords;
|
|
|
|
|
|
|
|
|
|
toast.loading('جاري تحديد الموقع...', { id: 'location' });
|
|
|
|
|
|
|
|
|
|
const response = await fetch(
|
|
|
|
|
`https://nominatim.openstreetmap.org/reverse?format=json&lat=${lat}&lon=${lng}&accept-language=ar`
|
|
|
|
|
);
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
|
|
|
|
setSelectedLocation({
|
|
|
|
|
lat: lat,
|
|
|
|
|
lng: lng,
|
|
|
|
|
address: data.display_name || 'موقع محدد'
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
setMapZoom(18);
|
|
|
|
|
toast.success('تم تحديد الموقع بنجاح!', { id: 'location' });
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('خطأ في تحديد الموقع:', error);
|
|
|
|
|
const [lat, lng] = coords;
|
|
|
|
|
setSelectedLocation({
|
|
|
|
|
lat: lat,
|
|
|
|
|
lng: lng,
|
|
|
|
|
address: 'موقع محدد'
|
|
|
|
|
});
|
|
|
|
|
setMapZoom(18);
|
|
|
|
|
toast.success('تم تحديد الموقع', { id: 'location' });
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
const confirmLocation = () => {
|
|
|
|
|
if (selectedLocation) {
|
|
|
|
|
setFormData({
|
|
|
|
|
...formData,
|
|
|
|
|
lat: selectedLocation.lat,
|
|
|
|
|
lng: selectedLocation.lng,
|
|
|
|
|
address: selectedLocation.address
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
toast.success('تم تأكيد الموقع بنجاح');
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const resetLocation = () => {
|
|
|
|
|
setSelectedLocation(null);
|
|
|
|
|
setFormData({
|
|
|
|
|
...formData,
|
|
|
|
|
lat: null,
|
|
|
|
|
lng: null,
|
|
|
|
|
address: ''
|
|
|
|
|
});
|
|
|
|
|
setMapZoom(15);
|
|
|
|
|
toast.info('تم إلغاء تحديد الموقع');
|
|
|
|
|
};
|
|
|
|
|
|
2026-03-30 01:15:57 +00:00
|
|
|
const handleImageUpload = async (files) => {
|
2026-03-17 20:36:59 +03:00
|
|
|
const newImages = Array.from(files);
|
|
|
|
|
|
|
|
|
|
if (formData.images.length + newImages.length > 10) {
|
|
|
|
|
toast.error('يمكنك رفع 10 صور كحد أقصى');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-30 01:15:57 +00:00
|
|
|
for (const file of newImages) {
|
2026-03-17 20:36:59 +03:00
|
|
|
if (!file.type.startsWith('image/')) {
|
|
|
|
|
toast.error('الرجاء اختيار صور صالحة فقط');
|
2026-03-30 01:15:57 +00:00
|
|
|
continue;
|
2026-03-17 20:36:59 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (file.size > 5 * 1024 * 1024) {
|
|
|
|
|
toast.error('حجم الصورة يجب أن يكون أقل من 5 ميجابايت');
|
2026-03-30 01:15:57 +00:00
|
|
|
continue;
|
2026-03-17 20:36:59 +03:00
|
|
|
}
|
|
|
|
|
|
2026-03-30 01:15:57 +00:00
|
|
|
// Show preview
|
2026-03-17 20:36:59 +03:00
|
|
|
const reader = new FileReader();
|
|
|
|
|
reader.onloadend = () => {
|
|
|
|
|
setImagePreviews(prev => [...prev, reader.result]);
|
|
|
|
|
};
|
|
|
|
|
reader.readAsDataURL(file);
|
|
|
|
|
|
2026-03-30 01:15:57 +00:00
|
|
|
setFormData(prev => ({
|
|
|
|
|
...prev,
|
|
|
|
|
images: [...prev.images, file]
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
// Upload to server immediately
|
|
|
|
|
try {
|
|
|
|
|
const path = await uploadPicture(file);
|
|
|
|
|
setUploadedImagePaths(prev => [...prev, path]);
|
|
|
|
|
console.log('[AddProperty] Image uploaded:', path);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error('[AddProperty] Image upload failed:', err);
|
|
|
|
|
toast.error('فشل رفع الصورة: ' + file.name);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-17 20:36:59 +03:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const removeImage = (index) => {
|
|
|
|
|
const newImages = [...formData.images];
|
|
|
|
|
newImages.splice(index, 1);
|
|
|
|
|
|
|
|
|
|
const newPreviews = [...imagePreviews];
|
|
|
|
|
newPreviews.splice(index, 1);
|
|
|
|
|
|
2026-03-30 01:15:57 +00:00
|
|
|
const newPaths = [...uploadedImagePaths];
|
|
|
|
|
newPaths.splice(index, 1);
|
|
|
|
|
|
|
|
|
|
setFormData(prev => ({ ...prev, images: newImages }));
|
2026-03-17 20:36:59 +03:00
|
|
|
setImagePreviews(newPreviews);
|
2026-03-30 01:15:57 +00:00
|
|
|
setUploadedImagePaths(newPaths);
|
2026-03-17 20:36:59 +03:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const toggleService = (serviceId) => {
|
2026-03-30 00:57:52 +00:00
|
|
|
setFormData(prev => {
|
|
|
|
|
const services = { ...prev.services };
|
|
|
|
|
services[serviceId] = !services[serviceId];
|
|
|
|
|
return { ...prev, services };
|
2026-03-17 20:36:59 +03:00
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
2026-03-30 00:57:52 +00:00
|
|
|
const updateServiceDetail = (serviceId, value) => {
|
|
|
|
|
setFormData(prev => ({
|
|
|
|
|
...prev,
|
|
|
|
|
serviceDetails: { ...prev.serviceDetails, [serviceId]: value }
|
|
|
|
|
}));
|
|
|
|
|
};
|
|
|
|
|
|
2026-03-17 20:36:59 +03:00
|
|
|
const toggleTerm = (termId) => {
|
2026-03-30 00:57:52 +00:00
|
|
|
setFormData(prev => {
|
|
|
|
|
const terms = { ...prev.terms };
|
|
|
|
|
terms[termId] = !terms[termId];
|
|
|
|
|
return { ...prev, terms };
|
2026-03-17 20:36:59 +03:00
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const incrementBedrooms = () => {
|
|
|
|
|
setFormData({
|
|
|
|
|
...formData,
|
|
|
|
|
bedrooms: formData.bedrooms + 1
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const decrementBedrooms = () => {
|
|
|
|
|
if (formData.bedrooms > 1) {
|
|
|
|
|
setFormData({
|
|
|
|
|
...formData,
|
|
|
|
|
bedrooms: formData.bedrooms - 1
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const incrementBathrooms = () => {
|
|
|
|
|
setFormData({
|
|
|
|
|
...formData,
|
|
|
|
|
bathrooms: formData.bathrooms + 1
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const decrementBathrooms = () => {
|
|
|
|
|
if (formData.bathrooms > 1) {
|
|
|
|
|
setFormData({
|
|
|
|
|
...formData,
|
|
|
|
|
bathrooms: formData.bathrooms - 1
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const incrementLivingRooms = () => {
|
|
|
|
|
setFormData({
|
|
|
|
|
...formData,
|
|
|
|
|
livingRooms: formData.livingRooms + 1
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const decrementLivingRooms = () => {
|
|
|
|
|
if (formData.livingRooms > 1) {
|
|
|
|
|
setFormData({
|
|
|
|
|
...formData,
|
|
|
|
|
livingRooms: formData.livingRooms - 1
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const validateStep = () => {
|
|
|
|
|
const newErrors = {};
|
|
|
|
|
|
|
|
|
|
switch(step) {
|
|
|
|
|
case 1:
|
|
|
|
|
if (!formData.propertyType) {
|
|
|
|
|
newErrors.propertyType = 'نوع العقار مطلوب';
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case 2:
|
|
|
|
|
if (!formData.bedrooms) {
|
|
|
|
|
newErrors.bedrooms = 'عدد الغرف مطلوب';
|
|
|
|
|
}
|
|
|
|
|
if (!formData.bathrooms) {
|
|
|
|
|
newErrors.bathrooms = 'عدد الحمامات مطلوب';
|
|
|
|
|
}
|
|
|
|
|
if (!formData.livingRooms) {
|
|
|
|
|
newErrors.livingRooms = 'عدد الصالونات مطلوب';
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case 3:
|
|
|
|
|
if (formData.offerType === 'daily' && !formData.dailyPrice) {
|
|
|
|
|
newErrors.dailyPrice = 'السعر اليومي مطلوب';
|
|
|
|
|
}
|
|
|
|
|
if (formData.offerType === 'monthly' && !formData.monthlyPrice) {
|
|
|
|
|
newErrors.monthlyPrice = 'السعر الشهري مطلوب';
|
|
|
|
|
}
|
|
|
|
|
if (formData.offerType === 'both') {
|
|
|
|
|
if (!formData.dailyPrice) newErrors.dailyPrice = 'السعر اليومي مطلوب';
|
|
|
|
|
if (!formData.monthlyPrice) newErrors.monthlyPrice = 'السعر الشهري مطلوب';
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case 4:
|
|
|
|
|
if (!formData.lat || !formData.lng) {
|
|
|
|
|
newErrors.location = 'الرجاء تحديد موقع العقار على الخريطة';
|
|
|
|
|
}
|
|
|
|
|
if (formData.images.length === 0) {
|
|
|
|
|
newErrors.images = 'يجب رفع صورة واحدة على الأقل';
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setErrors(newErrors);
|
|
|
|
|
return Object.keys(newErrors).length === 0;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleNext = () => {
|
|
|
|
|
if (validateStep()) {
|
|
|
|
|
setStep(step + 1);
|
|
|
|
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleBack = () => {
|
|
|
|
|
setStep(step - 1);
|
|
|
|
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleSubmit = async () => {
|
|
|
|
|
if (!validateStep()) return;
|
|
|
|
|
|
|
|
|
|
setIsLoading(true);
|
2026-03-28 18:00:44 +00:00
|
|
|
console.log('[AddProperty] Building RentPropertyDto payload...');
|
|
|
|
|
|
2026-03-29 15:48:48 +00:00
|
|
|
// Map UI property type to API BuildingType enum
|
|
|
|
|
const buildingTypeMap = { apartment: BuildingType.APARTMENT, villa: BuildingType.VILLA, suite: BuildingType.APARTMENT, room: BuildingType.APARTMENT };
|
|
|
|
|
|
|
|
|
|
// Map offer type to RentType enum: 0=Monthly, 1=Daily
|
|
|
|
|
const rentTypeMap = { daily: RentType.DAILY, monthly: RentType.MONTHLY, both: RentType.MONTHLY };
|
|
|
|
|
|
|
|
|
|
// Services: collect selected service enum names into array
|
|
|
|
|
const selectedServices = Object.entries(formData.services)
|
|
|
|
|
.filter(([, v]) => v)
|
|
|
|
|
.map(([k]) => k); // k is already the enum value (e.g. "Electricity")
|
2026-03-28 18:00:44 +00:00
|
|
|
|
2026-03-29 15:48:48 +00:00
|
|
|
// Terms: collect selected term enum names into array
|
|
|
|
|
const selectedTerms = Object.entries(formData.terms)
|
|
|
|
|
.filter(([, v]) => v)
|
|
|
|
|
.map(([k]) => k); // k is already the enum value (e.g. "NoSmoking")
|
|
|
|
|
|
|
|
|
|
// Build detailsJSON matching Flutter structure
|
2026-03-28 18:00:44 +00:00
|
|
|
const detailsJSON = JSON.stringify({
|
2026-03-29 15:48:48 +00:00
|
|
|
services: selectedServices,
|
|
|
|
|
serviceDetails: selectedServices.reduce((acc, s) => ({ ...acc, [s]: 'in general' }), {}),
|
|
|
|
|
terms: selectedTerms,
|
|
|
|
|
displayType: formData.offerType === 'both' ? 'Both' : formData.offerType === 'daily' ? 'Daily' : 'Monthly',
|
|
|
|
|
propertyCondition: formData.furnished ? 'Furnished' : 'Unfurnished',
|
|
|
|
|
photos: imagePreviews.map((_, i) => `photo_${i}.jpg`),
|
|
|
|
|
room: {
|
|
|
|
|
areaType: formData.propertyType === 'room' ? 'Shared room' : 'Private room',
|
|
|
|
|
peopleAllowed: String(formData.bedrooms),
|
|
|
|
|
entranceType: formData.propertyType === 'room' ? 'Shared entrance' : 'Private entrance',
|
|
|
|
|
bathroomType: formData.bathrooms > 1 ? 'Private' : 'Shared',
|
|
|
|
|
kitchenType: 'Not available',
|
|
|
|
|
hasRestrictedOwnerAreas: false,
|
|
|
|
|
languageDialect: '',
|
|
|
|
|
hasChildren: false,
|
|
|
|
|
hasPets: false,
|
|
|
|
|
dedicatedTo: 'Everyone',
|
|
|
|
|
visitorsAllowed: true,
|
|
|
|
|
quietTimesEnabled: false,
|
|
|
|
|
quietTimes: '',
|
|
|
|
|
}
|
2026-03-28 18:00:44 +00:00
|
|
|
});
|
2026-03-17 20:36:59 +03:00
|
|
|
|
2026-03-28 18:00:44 +00:00
|
|
|
const payload = {
|
2026-03-29 15:48:48 +00:00
|
|
|
propertyInformation: {
|
|
|
|
|
cordsX: formData.lat ? String(formData.lat) : '',
|
|
|
|
|
cordsY: formData.lng ? String(formData.lng) : '',
|
|
|
|
|
address: `${formData.city} - ${formData.district} - ${formData.address}`.trim(),
|
|
|
|
|
description: formData.description || '',
|
|
|
|
|
numberOfBathRooms: formData.bathrooms || 0,
|
|
|
|
|
numberOfRooms: (formData.bedrooms || 0) + (formData.livingRooms || 0),
|
|
|
|
|
numberOfBedRooms: formData.bedrooms || 0,
|
|
|
|
|
space: parseFloat(formData.space) || 0,
|
|
|
|
|
detailsJSON,
|
|
|
|
|
buildingType: buildingTypeMap[formData.propertyType] ?? BuildingType.APARTMENT,
|
|
|
|
|
status: 0,
|
|
|
|
|
propertyType: formData.furnished ? RentPropertyCondition.WITH_FURNITURE : RentPropertyCondition.WITHOUT_FURNITURE,
|
2026-03-30 00:57:52 +00:00
|
|
|
images: uploadedImagePaths,
|
2026-03-28 18:00:44 +00:00
|
|
|
},
|
2026-03-29 15:48:48 +00:00
|
|
|
deposit: parseFloat(formData.deposit) || 0,
|
|
|
|
|
monthlyRent: parseFloat(formData.monthlyPrice) || 0,
|
|
|
|
|
dailyRent: parseFloat(formData.dailyPrice) || 0,
|
|
|
|
|
rating: 0,
|
|
|
|
|
currencyId: selectedCurrencyId,
|
|
|
|
|
rentType: rentTypeMap[formData.offerType] ?? RentType.MONTHLY,
|
|
|
|
|
isSmokeAllow: !formData.terms[PropertyTerm.NO_SMOKING],
|
|
|
|
|
specializedFor: false,
|
|
|
|
|
isVisitorAllow: !formData.terms[PropertyTerm.NO_PARTIES],
|
|
|
|
|
type: formData.furnished ? RentPropertyType.FURNISHED : RentPropertyType.UNFURNISHED,
|
2026-03-28 18:00:44 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
console.log('[AddProperty] Payload:', JSON.stringify(payload, null, 2));
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const res = await addRentProperty(payload);
|
|
|
|
|
console.log('[AddProperty] API response:', res);
|
2026-03-17 20:36:59 +03:00
|
|
|
toast.success('تم إضافة العقار بنجاح!');
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
router.push('/owner/properties');
|
|
|
|
|
}, 1500);
|
2026-03-28 18:00:44 +00:00
|
|
|
} catch (err) {
|
|
|
|
|
console.error('[AddProperty] API error:', err);
|
|
|
|
|
toast.error(err.message || 'فشل في إضافة العقار');
|
|
|
|
|
} finally {
|
|
|
|
|
setIsLoading(false);
|
|
|
|
|
}
|
2026-03-17 20:36:59 +03:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const fadeInUp = {
|
|
|
|
|
initial: { opacity: 0, y: 20 },
|
|
|
|
|
animate: { opacity: 1, y: 0 },
|
|
|
|
|
transition: { duration: 0.5 }
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="min-h-screen bg-gray-50 py-8">
|
|
|
|
|
<Toaster position="top-center" reverseOrder={false} />
|
|
|
|
|
|
|
|
|
|
<div className="container mx-auto px-4 max-w-4xl">
|
|
|
|
|
<div className="mb-8">
|
|
|
|
|
<div className="flex items-center justify-between mb-4">
|
|
|
|
|
<Link
|
|
|
|
|
href="/owner/properties"
|
|
|
|
|
className="flex items-center gap-2 text-gray-600 hover:text-amber-600 transition-colors group"
|
|
|
|
|
>
|
|
|
|
|
<ArrowLeft className="w-4 h-4 group-hover:-translate-x-1 transition-transform" />
|
|
|
|
|
<span>العودة للعقارات</span>
|
|
|
|
|
</Link>
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<span className="text-sm font-medium text-amber-600">خطوة {step} من {totalSteps}</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="flex gap-2">
|
|
|
|
|
{[1, 2, 3, 4].map((s) => (
|
|
|
|
|
<motion.div
|
|
|
|
|
key={s}
|
|
|
|
|
className={`h-2 flex-1 rounded-full ${
|
|
|
|
|
s <= step ? 'bg-amber-500' : 'bg-gray-200'
|
|
|
|
|
}`}
|
|
|
|
|
animate={{ scaleX: s <= step ? 1 : 0.5 }}
|
|
|
|
|
/>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="flex justify-between mt-2 text-xs text-gray-500">
|
|
|
|
|
<span>معلومات العقار</span>
|
|
|
|
|
<span>التفاصيل والخدمات</span>
|
|
|
|
|
<span>السعر</span>
|
|
|
|
|
<span>الموقع والصور</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<motion.div
|
|
|
|
|
key={step}
|
|
|
|
|
initial={{ opacity: 0, x: 20 }}
|
|
|
|
|
animate={{ opacity: 1, x: 0 }}
|
|
|
|
|
exit={{ opacity: 0, x: -20 }}
|
|
|
|
|
transition={{ duration: 0.3 }}
|
|
|
|
|
className="bg-white rounded-2xl shadow-xl p-6 md:p-8"
|
|
|
|
|
>
|
|
|
|
|
{step === 1 && (
|
|
|
|
|
<motion.div variants={fadeInUp} className="space-y-8">
|
|
|
|
|
<div className="text-center mb-6">
|
|
|
|
|
<div className="w-20 h-20 bg-amber-100 rounded-2xl flex items-center justify-center mx-auto mb-4">
|
|
|
|
|
<Home className="w-10 h-10 text-amber-600" />
|
|
|
|
|
</div>
|
|
|
|
|
<h2 className="text-2xl font-bold text-gray-900 mb-2">معلومات العقار</h2>
|
|
|
|
|
<p className="text-gray-600">اختر نوع العقار والحالة</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
<label className="block text-sm font-medium text-gray-700 mb-3">
|
|
|
|
|
نوع العقار <span className="text-red-500">*</span>
|
|
|
|
|
</label>
|
|
|
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
|
|
|
|
{propertyTypes.map((type) => {
|
|
|
|
|
const Icon = type.icon;
|
|
|
|
|
return (
|
|
|
|
|
<button
|
|
|
|
|
key={type.id}
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={() => setFormData({...formData, propertyType: type.id})}
|
|
|
|
|
className={`p-4 border rounded-xl flex flex-col items-center gap-2 transition-all ${
|
|
|
|
|
formData.propertyType === type.id
|
|
|
|
|
? 'border-amber-500 bg-amber-50 text-amber-700'
|
|
|
|
|
: 'border-gray-200 hover:border-amber-200 hover:bg-amber-50/50'
|
|
|
|
|
}`}
|
|
|
|
|
>
|
|
|
|
|
<Icon className="w-6 h-6" />
|
|
|
|
|
<span className="text-sm font-medium">{type.label}</span>
|
|
|
|
|
</button>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
{errors.propertyType && (
|
|
|
|
|
<p className="text-red-500 text-sm mt-2">{errors.propertyType}</p>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
<label className="block text-sm font-medium text-gray-700 mb-3">
|
|
|
|
|
حالة العقار
|
|
|
|
|
</label>
|
|
|
|
|
<div className="flex gap-4">
|
|
|
|
|
<label className="flex items-center gap-2 p-3 border rounded-xl cursor-pointer hover:bg-gray-50 flex-1">
|
|
|
|
|
<input
|
|
|
|
|
type="radio"
|
|
|
|
|
name="furnished"
|
|
|
|
|
checked={formData.furnished === true}
|
|
|
|
|
onChange={() => setFormData({...formData, furnished: true})}
|
|
|
|
|
className="w-4 h-4 text-amber-500"
|
|
|
|
|
/>
|
|
|
|
|
<span className="text-gray-700">مفروش</span>
|
|
|
|
|
</label>
|
|
|
|
|
<label className="flex items-center gap-2 p-3 border rounded-xl cursor-pointer hover:bg-gray-50 flex-1">
|
|
|
|
|
<input
|
|
|
|
|
type="radio"
|
|
|
|
|
name="furnished"
|
|
|
|
|
checked={formData.furnished === false}
|
|
|
|
|
onChange={() => setFormData({...formData, furnished: false})}
|
|
|
|
|
className="w-4 h-4 text-amber-500"
|
|
|
|
|
/>
|
|
|
|
|
<span className="text-gray-700">غير مفروش</span>
|
|
|
|
|
</label>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
|
|
|
وصف إضافي (اختياري)
|
|
|
|
|
</label>
|
|
|
|
|
<textarea
|
|
|
|
|
value={formData.description}
|
|
|
|
|
onChange={(e) => setFormData({...formData, description: e.target.value})}
|
|
|
|
|
rows="4"
|
|
|
|
|
className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500"
|
|
|
|
|
placeholder="أضف وصفاً إضافياً للعقار..."
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</motion.div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{step === 2 && (
|
|
|
|
|
<motion.div variants={fadeInUp} className="space-y-8">
|
|
|
|
|
<div className="text-center mb-6">
|
|
|
|
|
<div className="w-20 h-20 bg-amber-100 rounded-2xl flex items-center justify-center mx-auto mb-4">
|
|
|
|
|
<Layers className="w-10 h-10 text-amber-600" />
|
|
|
|
|
</div>
|
|
|
|
|
<h2 className="text-2xl font-bold text-gray-900 mb-2">تفاصيل العقار</h2>
|
|
|
|
|
<p className="text-gray-600">أدخل التفاصيل والخدمات المتاحة</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
|
|
|
<div>
|
|
|
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
|
|
|
عدد الغرف <span className="text-red-500">*</span>
|
|
|
|
|
</label>
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={decrementBedrooms}
|
|
|
|
|
className="w-10 h-10 bg-gray-100 rounded-lg flex items-center justify-center hover:bg-gray-200 transition-colors"
|
|
|
|
|
>
|
|
|
|
|
<Minus className="w-4 h-4" />
|
|
|
|
|
</button>
|
|
|
|
|
<div className="flex-1 text-center font-bold text-xl">
|
|
|
|
|
{formData.bedrooms}
|
|
|
|
|
</div>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={incrementBedrooms}
|
|
|
|
|
className="w-10 h-10 bg-gray-100 rounded-lg flex items-center justify-center hover:bg-gray-200 transition-colors"
|
|
|
|
|
>
|
|
|
|
|
<Plus className="w-4 h-4" />
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
{errors.bedrooms && (
|
|
|
|
|
<p className="text-red-500 text-sm mt-1">{errors.bedrooms}</p>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
|
|
|
عدد الحمامات <span className="text-red-500">*</span>
|
|
|
|
|
</label>
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={decrementBathrooms}
|
|
|
|
|
className="w-10 h-10 bg-gray-100 rounded-lg flex items-center justify-center hover:bg-gray-200"
|
|
|
|
|
>
|
|
|
|
|
<Minus className="w-4 h-4" />
|
|
|
|
|
</button>
|
|
|
|
|
<div className="flex-1 text-center font-bold text-xl">
|
|
|
|
|
{formData.bathrooms}
|
|
|
|
|
</div>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={incrementBathrooms}
|
|
|
|
|
className="w-10 h-10 bg-gray-100 rounded-lg flex items-center justify-center hover:bg-gray-200"
|
|
|
|
|
>
|
|
|
|
|
<Plus className="w-4 h-4" />
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
{errors.bathrooms && (
|
|
|
|
|
<p className="text-red-500 text-sm mt-1">{errors.bathrooms}</p>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
|
|
|
عدد الصالونات <span className="text-red-500">*</span>
|
|
|
|
|
</label>
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={decrementLivingRooms}
|
|
|
|
|
className="w-10 h-10 bg-gray-100 rounded-lg flex items-center justify-center hover:bg-gray-200"
|
|
|
|
|
>
|
|
|
|
|
<Minus className="w-4 h-4" />
|
|
|
|
|
</button>
|
|
|
|
|
<div className="flex-1 text-center font-bold text-xl">
|
|
|
|
|
{formData.livingRooms}
|
|
|
|
|
</div>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={incrementLivingRooms}
|
|
|
|
|
className="w-10 h-10 bg-gray-100 rounded-lg flex items-center justify-center hover:bg-gray-200"
|
|
|
|
|
>
|
|
|
|
|
<Plus className="w-4 h-4" />
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
{errors.livingRooms && (
|
|
|
|
|
<p className="text-red-500 text-sm mt-1">{errors.livingRooms}</p>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div>
|
2026-03-30 01:01:42 +00:00
|
|
|
<h3 className="text-lg font-bold text-gray-900 mb-4">الخدمات المتوفرة <span className="text-red-500">*</span></h3>
|
|
|
|
|
<div className="space-y-3">
|
2026-03-17 20:36:59 +03:00
|
|
|
{serviceList.map((service) => {
|
|
|
|
|
const Icon = service.icon;
|
2026-03-30 01:01:42 +00:00
|
|
|
const isSelected = formData.services[service.id];
|
2026-03-17 20:36:59 +03:00
|
|
|
return (
|
2026-03-30 01:01:42 +00:00
|
|
|
<div key={service.id} className={`border rounded-xl transition-all ${isSelected ? 'border-amber-500 bg-amber-50' : 'border-gray-200'}`}>
|
|
|
|
|
<label className="flex items-center gap-3 p-3 cursor-pointer">
|
|
|
|
|
<input
|
|
|
|
|
type="checkbox"
|
|
|
|
|
checked={isSelected}
|
|
|
|
|
onChange={() => toggleService(service.id)}
|
|
|
|
|
className="w-4 h-4 text-amber-500 rounded"
|
|
|
|
|
/>
|
|
|
|
|
<Icon className={`w-5 h-5 ${isSelected ? 'text-amber-600' : 'text-gray-400'}`} />
|
|
|
|
|
<span className={`text-sm font-medium ${isSelected ? 'text-amber-700' : 'text-gray-600'}`}>
|
|
|
|
|
{service.label}
|
|
|
|
|
</span>
|
|
|
|
|
</label>
|
|
|
|
|
{isSelected && (
|
|
|
|
|
<div className="px-3 pb-3">
|
|
|
|
|
<input
|
|
|
|
|
type="text"
|
|
|
|
|
value={formData.serviceDetails[service.id] || ''}
|
|
|
|
|
onChange={(e) => updateServiceDetail(service.id, e.target.value)}
|
|
|
|
|
className="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-amber-500"
|
|
|
|
|
placeholder="تفاصيل الخدمة (مثال: في جميع الغرف)"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
2026-03-17 20:36:59 +03:00
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
<h3 className="text-lg font-bold text-gray-900 mb-4">شروط استخدام العقار</h3>
|
|
|
|
|
<div className="grid grid-cols-2 md:grid-cols-3 gap-3">
|
|
|
|
|
{termsList.map((term) => {
|
|
|
|
|
const Icon = term.icon;
|
|
|
|
|
return (
|
|
|
|
|
<label
|
|
|
|
|
key={term.id}
|
|
|
|
|
className={`flex items-center gap-2 p-3 border rounded-xl cursor-pointer transition-all ${
|
|
|
|
|
formData.terms[term.id]
|
|
|
|
|
? 'border-amber-500 bg-amber-50'
|
|
|
|
|
: 'border-gray-200 hover:border-amber-200 hover:bg-amber-50/50'
|
|
|
|
|
}`}
|
|
|
|
|
>
|
|
|
|
|
<input
|
|
|
|
|
type="checkbox"
|
|
|
|
|
checked={formData.terms[term.id]}
|
|
|
|
|
onChange={() => toggleTerm(term.id)}
|
|
|
|
|
className="hidden"
|
|
|
|
|
/>
|
|
|
|
|
<Icon className={`w-5 h-5 ${
|
|
|
|
|
formData.terms[term.id] ? 'text-amber-600' : 'text-gray-400'
|
|
|
|
|
}`} />
|
|
|
|
|
<span className={`text-sm ${
|
|
|
|
|
formData.terms[term.id] ? 'text-amber-700' : 'text-gray-600'
|
|
|
|
|
}`}>
|
|
|
|
|
{term.label}
|
|
|
|
|
</span>
|
|
|
|
|
</label>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</motion.div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{step === 3 && (
|
|
|
|
|
<motion.div variants={fadeInUp} className="space-y-8">
|
|
|
|
|
<div className="text-center mb-6">
|
|
|
|
|
<div className="w-20 h-20 bg-amber-100 rounded-2xl flex items-center justify-center mx-auto mb-4">
|
|
|
|
|
<DollarSign className="w-10 h-10 text-amber-600" />
|
|
|
|
|
</div>
|
|
|
|
|
<h2 className="text-2xl font-bold text-gray-900 mb-2">نوع العرض والسعر</h2>
|
|
|
|
|
<p className="text-gray-600">اختر نوع العرض وحدد السعر المناسب</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
<label className="block text-sm font-medium text-gray-700 mb-3">
|
|
|
|
|
نوع العرض <span className="text-red-500">*</span>
|
|
|
|
|
</label>
|
|
|
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
|
|
|
|
{offerTypes.map((type) => {
|
|
|
|
|
const Icon = type.icon;
|
|
|
|
|
return (
|
|
|
|
|
<button
|
|
|
|
|
key={type.id}
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={() => setFormData({...formData, offerType: type.id})}
|
|
|
|
|
className={`p-4 border rounded-xl flex flex-col items-center gap-2 transition-all ${
|
|
|
|
|
formData.offerType === type.id
|
|
|
|
|
? 'border-amber-500 bg-amber-50 text-amber-700'
|
|
|
|
|
: 'border-gray-200 hover:border-amber-200 hover:bg-amber-50/50'
|
|
|
|
|
}`}
|
|
|
|
|
>
|
|
|
|
|
<Icon className="w-6 h-6" />
|
|
|
|
|
<span className="text-sm font-medium">{type.label}</span>
|
|
|
|
|
</button>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-03-28 19:40:03 +00:00
|
|
|
{/* Currency dropdown */}
|
2026-03-29 15:48:48 +00:00
|
|
|
<div>
|
|
|
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
|
|
|
العملة <span className="text-red-500">*</span>
|
|
|
|
|
</label>
|
|
|
|
|
<select
|
|
|
|
|
value={selectedCurrencyId}
|
|
|
|
|
onChange={(e) => setSelectedCurrencyId(parseInt(e.target.value))}
|
|
|
|
|
className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500"
|
|
|
|
|
>
|
|
|
|
|
{Object.entries(CurrencyLabels).map(([id, label]) => (
|
|
|
|
|
<option key={id} value={id}>
|
|
|
|
|
{label}
|
|
|
|
|
</option>
|
|
|
|
|
))}
|
|
|
|
|
</select>
|
|
|
|
|
</div>
|
2026-03-28 19:40:03 +00:00
|
|
|
|
|
|
|
|
{/* Deposit field */}
|
|
|
|
|
<div>
|
|
|
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
|
|
|
مبلغ الضمان (العربون)
|
|
|
|
|
</label>
|
|
|
|
|
<div className="relative">
|
|
|
|
|
<DollarSign className="absolute right-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-gray-400" />
|
|
|
|
|
<input
|
|
|
|
|
type="number"
|
|
|
|
|
value={formData.deposit || ''}
|
|
|
|
|
onChange={(e) => setFormData({...formData, deposit: e.target.value})}
|
|
|
|
|
className="w-full pr-12 pl-4 py-3 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500"
|
|
|
|
|
placeholder="مثال: 500000"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-03-17 20:36:59 +03:00
|
|
|
<AnimatePresence mode="wait">
|
|
|
|
|
{(formData.offerType === 'daily' || formData.offerType === 'both') && (
|
|
|
|
|
<motion.div
|
|
|
|
|
key="daily"
|
|
|
|
|
initial={{ opacity: 0, height: 0 }}
|
|
|
|
|
animate={{ opacity: 1, height: 'auto' }}
|
|
|
|
|
exit={{ opacity: 0, height: 0 }}
|
|
|
|
|
className="space-y-4"
|
|
|
|
|
>
|
|
|
|
|
<div>
|
|
|
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
|
|
|
السعر اليومي (ل.س) <span className="text-red-500">*</span>
|
|
|
|
|
</label>
|
|
|
|
|
<div className="relative">
|
|
|
|
|
<DollarSign className="absolute right-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-gray-400" />
|
|
|
|
|
<input
|
|
|
|
|
type="number"
|
|
|
|
|
value={formData.dailyPrice}
|
|
|
|
|
onChange={(e) => setFormData({...formData, dailyPrice: e.target.value})}
|
|
|
|
|
className={`w-full pr-12 pl-4 py-3 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 ${
|
|
|
|
|
errors.dailyPrice ? 'border-red-500' : 'border-gray-300'
|
|
|
|
|
}`}
|
|
|
|
|
placeholder="مثال: 50000"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
{errors.dailyPrice && (
|
|
|
|
|
<p className="text-red-500 text-sm mt-1">{errors.dailyPrice}</p>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</motion.div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{(formData.offerType === 'monthly' || formData.offerType === 'both') && (
|
|
|
|
|
<motion.div
|
|
|
|
|
key="monthly"
|
|
|
|
|
initial={{ opacity: 0, height: 0 }}
|
|
|
|
|
animate={{ opacity: 1, height: 'auto' }}
|
|
|
|
|
exit={{ opacity: 0, height: 0 }}
|
|
|
|
|
className="space-y-4"
|
|
|
|
|
>
|
|
|
|
|
<div>
|
|
|
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
|
|
|
السعر الشهري (ل.س) <span className="text-red-500">*</span>
|
|
|
|
|
</label>
|
|
|
|
|
<div className="relative">
|
|
|
|
|
<DollarSign className="absolute right-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-gray-400" />
|
|
|
|
|
<input
|
|
|
|
|
type="number"
|
|
|
|
|
value={formData.monthlyPrice}
|
|
|
|
|
onChange={(e) => setFormData({...formData, monthlyPrice: e.target.value})}
|
|
|
|
|
className={`w-full pr-12 pl-4 py-3 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 ${
|
|
|
|
|
errors.monthlyPrice ? 'border-red-500' : 'border-gray-300'
|
|
|
|
|
}`}
|
|
|
|
|
placeholder="مثال: 1000000"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
{errors.monthlyPrice && (
|
|
|
|
|
<p className="text-red-500 text-sm mt-1">{errors.monthlyPrice}</p>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</motion.div>
|
|
|
|
|
)}
|
|
|
|
|
</AnimatePresence>
|
|
|
|
|
</motion.div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{step === 4 && (
|
|
|
|
|
<motion.div variants={fadeInUp} className="space-y-8">
|
|
|
|
|
<div className="text-center mb-6">
|
|
|
|
|
<div className="w-20 h-20 bg-amber-100 rounded-2xl flex items-center justify-center mx-auto mb-4">
|
|
|
|
|
<MapPin className="w-10 h-10 text-amber-600" />
|
|
|
|
|
</div>
|
|
|
|
|
<h2 className="text-2xl font-bold text-gray-900 mb-2">الموقع والصور</h2>
|
|
|
|
|
<p className="text-gray-600">حدد موقع العقار وأضف الصور</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
<h3 className="text-lg font-bold text-gray-900 mb-4">حدد موقع العقار على الخريطة</h3>
|
|
|
|
|
|
|
|
|
|
<div className="flex gap-2 mb-4">
|
|
|
|
|
<div className="flex-1 relative">
|
|
|
|
|
<input
|
|
|
|
|
type="text"
|
|
|
|
|
value={searchQuery}
|
|
|
|
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
|
|
|
onKeyPress={(e) => e.key === 'Enter' && handleSearch()}
|
|
|
|
|
placeholder="ابحث عن عنوان..."
|
|
|
|
|
className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 pr-12"
|
|
|
|
|
/>
|
|
|
|
|
<Search className="absolute left-4 top-1/2 transform -translate-y-1/2 w-5 h-5 text-gray-400" />
|
|
|
|
|
</div>
|
|
|
|
|
<button
|
|
|
|
|
onClick={handleSearch}
|
|
|
|
|
className="px-6 py-3 bg-amber-500 text-white rounded-xl hover:bg-amber-600 transition-colors"
|
|
|
|
|
>
|
|
|
|
|
بحث
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="relative w-full h-96 rounded-xl overflow-hidden border-2 border-gray-200 mb-4">
|
|
|
|
|
{mapLoaded && (
|
|
|
|
|
<MapContainer
|
|
|
|
|
center={mapCenter}
|
|
|
|
|
zoom={mapZoom}
|
|
|
|
|
style={{ height: '100%', width: '100%' }}
|
|
|
|
|
className="z-0"
|
|
|
|
|
doubleClickZoom={false}
|
|
|
|
|
>
|
|
|
|
|
<TileLayer
|
|
|
|
|
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
|
|
|
|
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<MapClickHandler onMapClick={handleMapClick} />
|
|
|
|
|
|
|
|
|
|
{selectedLocation && (
|
|
|
|
|
<Marker
|
|
|
|
|
position={[selectedLocation.lat, selectedLocation.lng]}
|
|
|
|
|
draggable={true}
|
|
|
|
|
eventHandlers={{
|
|
|
|
|
dragend: async (e) => {
|
|
|
|
|
const marker = e.target;
|
|
|
|
|
const position = marker.getLatLng();
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const response = await fetch(
|
|
|
|
|
`https://nominatim.openstreetmap.org/reverse?format=json&lat=${position.lat}&lon=${position.lng}&accept-language=ar`
|
|
|
|
|
);
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
|
|
|
|
setSelectedLocation({
|
|
|
|
|
lat: position.lat,
|
|
|
|
|
lng: position.lng,
|
|
|
|
|
address: data.display_name || 'موقع محدد'
|
|
|
|
|
});
|
|
|
|
|
} catch (error) {
|
|
|
|
|
setSelectedLocation({
|
|
|
|
|
lat: position.lat,
|
|
|
|
|
lng: position.lng,
|
|
|
|
|
address: 'موقع محدد'
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<Popup>
|
|
|
|
|
<div className="text-right p-2 max-w-xs">
|
|
|
|
|
<p className="font-bold text-sm mb-1">موقع العقار</p>
|
|
|
|
|
<p className="text-xs text-gray-600">{selectedLocation.address}</p>
|
|
|
|
|
</div>
|
|
|
|
|
</Popup>
|
|
|
|
|
</Marker>
|
|
|
|
|
)}
|
|
|
|
|
</MapContainer>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{selectedLocation && !formData.lat && (
|
|
|
|
|
<button
|
|
|
|
|
onClick={confirmLocation}
|
|
|
|
|
className="w-full mb-4 bg-green-500 text-white py-3 rounded-xl font-medium hover:bg-green-600 transition-colors flex items-center justify-center gap-2"
|
|
|
|
|
>
|
|
|
|
|
<CheckCircle className="w-5 h-5" />
|
|
|
|
|
تأكيد هذا الموقع
|
|
|
|
|
</button>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{formData.lat && (
|
|
|
|
|
<div className="bg-green-50 border border-green-200 rounded-xl p-4 mb-4">
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<CheckCircle className="w-5 h-5 text-green-600" />
|
|
|
|
|
<span className="text-green-800 font-medium">تم تأكيد الموقع:</span>
|
|
|
|
|
</div>
|
|
|
|
|
<p className="text-green-700 text-sm mt-2 line-clamp-2">{formData.address}</p>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{errors.location && (
|
|
|
|
|
<p className="text-red-500 text-sm text-center mb-4">{errors.location}</p>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
<h3 className="text-lg font-bold text-gray-900 mb-4">صور العقار</h3>
|
|
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
onClick={() => fileInputRef.current?.click()}
|
|
|
|
|
className={`border-2 border-dashed rounded-xl p-8 text-center cursor-pointer transition-all ${
|
|
|
|
|
errors.images ? 'border-red-500 bg-red-50' : 'border-gray-300 hover:border-amber-500 hover:bg-amber-50'
|
|
|
|
|
}`}
|
|
|
|
|
>
|
|
|
|
|
<input
|
|
|
|
|
ref={fileInputRef}
|
|
|
|
|
type="file"
|
|
|
|
|
multiple
|
|
|
|
|
accept="image/*"
|
|
|
|
|
onChange={(e) => handleImageUpload(e.target.files)}
|
|
|
|
|
className="hidden"
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<Upload className="w-12 h-12 text-gray-400 mx-auto mb-3" />
|
|
|
|
|
<p className="text-gray-600 font-medium">اضغط لرفع الصور</p>
|
|
|
|
|
<p className="text-xs text-gray-500 mt-2">
|
|
|
|
|
JPEG, PNG, JPG • حتى 5MB • 800x600 بكسل • حد أقصى 10 صور
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{errors.images && (
|
|
|
|
|
<p className="text-red-500 text-sm text-center mt-2">{errors.images}</p>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{imagePreviews.length > 0 && (
|
|
|
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mt-6">
|
|
|
|
|
{imagePreviews.map((preview, index) => (
|
|
|
|
|
<motion.div
|
|
|
|
|
key={index}
|
|
|
|
|
initial={{ opacity: 0, scale: 0.9 }}
|
|
|
|
|
animate={{ opacity: 1, scale: 1 }}
|
|
|
|
|
className="relative group aspect-square"
|
|
|
|
|
>
|
|
|
|
|
<Image
|
|
|
|
|
src={preview}
|
|
|
|
|
alt={`Property ${index + 1}`}
|
|
|
|
|
fill
|
|
|
|
|
className="object-cover rounded-lg"
|
|
|
|
|
/>
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => removeImage(index)}
|
|
|
|
|
className="absolute -top-2 -right-2 w-6 h-6 bg-red-500 rounded-full flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity"
|
|
|
|
|
>
|
|
|
|
|
<X className="w-4 h-4 text-white" />
|
|
|
|
|
</button>
|
|
|
|
|
</motion.div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</motion.div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
<div className="flex gap-3 mt-8 pt-6 border-t border-gray-200">
|
|
|
|
|
{step > 1 && (
|
|
|
|
|
<button
|
|
|
|
|
onClick={handleBack}
|
|
|
|
|
className="flex-1 py-3 px-4 bg-gray-100 text-gray-700 rounded-xl font-medium hover:bg-gray-200 transition-colors flex items-center justify-center gap-2"
|
|
|
|
|
>
|
|
|
|
|
<ChevronRight className="w-5 h-5" />
|
|
|
|
|
السابق
|
|
|
|
|
</button>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{step < totalSteps ? (
|
|
|
|
|
<button
|
|
|
|
|
onClick={handleNext}
|
|
|
|
|
className={`flex-1 py-3 px-4 bg-amber-500 text-white rounded-xl font-medium hover:bg-amber-600 transition-colors flex items-center justify-center gap-2 ${
|
|
|
|
|
step === 1 ? 'w-full' : ''
|
|
|
|
|
}`}
|
|
|
|
|
>
|
|
|
|
|
التالي
|
|
|
|
|
<ChevronLeft className="w-5 h-5" />
|
|
|
|
|
</button>
|
|
|
|
|
) : (
|
|
|
|
|
<button
|
|
|
|
|
onClick={handleSubmit}
|
|
|
|
|
disabled={isLoading}
|
|
|
|
|
className="flex-1 py-3 px-4 bg-gradient-to-r from-amber-500 to-amber-600 text-white rounded-xl font-medium hover:from-amber-600 hover:to-amber-700 transition-all disabled:opacity-50 flex items-center justify-center gap-2"
|
|
|
|
|
>
|
|
|
|
|
{isLoading ? (
|
|
|
|
|
<>
|
|
|
|
|
<Loader2 className="w-5 h-5 animate-spin" />
|
|
|
|
|
جاري الحفظ...
|
|
|
|
|
</>
|
|
|
|
|
) : (
|
|
|
|
|
<>
|
|
|
|
|
<Save className="w-5 h-5" />
|
|
|
|
|
حفظ العقار
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
</button>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</motion.div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|