the best in the west is mouaz
All checks were successful
Build frontend / build (push) Successful in 55s
All checks were successful
Build frontend / build (push) Successful in 55s
This commit is contained in:
@ -2,7 +2,7 @@
|
||||
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import Link from 'next/link';
|
||||
import Image from 'next/image';
|
||||
import dynamic from 'next/dynamic';
|
||||
@ -48,10 +48,11 @@ import {
|
||||
Minus,
|
||||
Save,
|
||||
Wind,
|
||||
Move
|
||||
Move,
|
||||
Trees
|
||||
} from 'lucide-react';
|
||||
import toast, { Toaster } from 'react-hot-toast';
|
||||
import { addRentProperty, getCurrencies, uploadPicture } from '../../../utils/api';
|
||||
import { addRentProperty, addSaleProperty, getCurrencies, uploadPicture } from '../../../utils/api';
|
||||
import {
|
||||
BuildingType,
|
||||
RentPropertyCondition,
|
||||
@ -85,12 +86,14 @@ function MapClickHandler({ onMapClick }) {
|
||||
|
||||
export default function AddPropertyPage() {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const purpose = searchParams.get('purpose') || 'rent';
|
||||
|
||||
const [step, setStep] = useState(1);
|
||||
const totalSteps = 4;
|
||||
const totalSteps = purpose === 'sale' ? 4 : 4;
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
propertyType: 'apartment', // apartment, villa, suite, room
|
||||
propertyType: 'apartment',
|
||||
|
||||
furnished: false,
|
||||
|
||||
@ -152,8 +155,13 @@ export default function AddPropertyPage() {
|
||||
const propertyTypes = [
|
||||
{ id: 'apartment', label: 'شقة', icon: Building },
|
||||
{ id: 'villa', label: 'فيلا', icon: Home },
|
||||
{ id: 'suite', label: 'سويت', icon: Sofa },
|
||||
{ id: 'room', label: 'غرفة ضمن شقة', icon: DoorOpen }
|
||||
{ id: 'sweet', label: 'سويت', icon: Sofa },
|
||||
{ id: 'room', label: 'غرفة', icon: DoorOpen },
|
||||
{ id: 'studio', label: 'استوديو', icon: Sofa },
|
||||
{ id: 'office', label: 'مكتب', icon: Building },
|
||||
{ id: 'farms', label: 'مزرعة', icon: Trees },
|
||||
{ id: 'shop', label: 'متجر', icon: Warehouse },
|
||||
{ id: 'warehouse', label: 'مستودع', icon: Warehouse },
|
||||
];
|
||||
|
||||
const serviceList = [
|
||||
@ -493,15 +501,19 @@ const handleMapClick = async (coords) => {
|
||||
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 = 'السعر الشهري مطلوب';
|
||||
if (purpose === 'sale') {
|
||||
if (!formData.salePrice) newErrors.salePrice = 'سعر البيع مطلوب';
|
||||
} else {
|
||||
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;
|
||||
|
||||
@ -535,25 +547,18 @@ const handleMapClick = async (coords) => {
|
||||
if (!validateStep()) return;
|
||||
|
||||
setIsLoading(true);
|
||||
console.log('[AddProperty] Building RentPropertyDto payload...');
|
||||
|
||||
// Map UI property type to API BuildingType enum
|
||||
const buildingTypeMap = { apartment: BuildingType.APARTMENT, villa: BuildingType.VILLA, suite: BuildingType.APARTMENT, room: BuildingType.APARTMENT };
|
||||
const buildingTypeMap = { apartment: BuildingType.APARTMENT, villa: BuildingType.VILLA, sweet: BuildingType.SWEET, suite: BuildingType.SWEET, room: BuildingType.ROOM, studio: BuildingType.STUDIO, office: BuildingType.OFFICE, farms: BuildingType.FARMS, shop: BuildingType.SHOP, warehouse: BuildingType.WAREHOUSE };
|
||||
|
||||
// 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")
|
||||
.map(([k]) => k);
|
||||
|
||||
// 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")
|
||||
.map(([k]) => k);
|
||||
|
||||
// Build detailsJSON matching Flutter structure
|
||||
const detailsJSON = JSON.stringify({
|
||||
services: selectedServices,
|
||||
serviceDetails: selectedServices.reduce((acc, s) => ({ ...acc, [s]: 'in general' }), {}),
|
||||
@ -578,40 +583,53 @@ const handleMapClick = async (coords) => {
|
||||
}
|
||||
});
|
||||
|
||||
const payload = {
|
||||
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,
|
||||
images: uploadedImagePaths,
|
||||
},
|
||||
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,
|
||||
const propInfo = {
|
||||
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,
|
||||
images: uploadedImagePaths,
|
||||
};
|
||||
|
||||
console.log('[AddProperty] Payload:', JSON.stringify(payload, null, 2));
|
||||
|
||||
try {
|
||||
const res = await addRentProperty(payload);
|
||||
console.log('[AddProperty] API response:', res);
|
||||
toast.success('تم إضافة العقار بنجاح!');
|
||||
if (purpose === 'sale') {
|
||||
const payload = {
|
||||
propInfo,
|
||||
price: parseFloat(formData.salePrice) || 0,
|
||||
currencyId: selectedCurrencyId,
|
||||
};
|
||||
console.log('[AddProperty] Sale payload:', JSON.stringify(payload, null, 2));
|
||||
const res = await addSaleProperty(payload);
|
||||
console.log('[AddProperty] Sale API response:', res);
|
||||
toast.success('تم إضافة عقار للبيع بنجاح!');
|
||||
} else {
|
||||
const rentTypeMap = { daily: RentType.DAILY, monthly: RentType.MONTHLY, both: RentType.MONTHLY };
|
||||
const payload = {
|
||||
propertyInformation: propInfo,
|
||||
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,
|
||||
};
|
||||
console.log('[AddProperty] Rent payload:', JSON.stringify(payload, null, 2));
|
||||
const res = await addRentProperty(payload);
|
||||
console.log('[AddProperty] Rent API response:', res);
|
||||
toast.success('تم إضافة عقار للإيجار بنجاح!');
|
||||
}
|
||||
setTimeout(() => {
|
||||
router.push('/owner/properties');
|
||||
}, 1500);
|
||||
@ -663,7 +681,7 @@ const handleMapClick = async (coords) => {
|
||||
<div className="flex justify-between mt-2 text-xs text-gray-500">
|
||||
<span>معلومات العقار</span>
|
||||
<span>التفاصيل والخدمات</span>
|
||||
<span>السعر</span>
|
||||
<span>{purpose === 'sale' ? 'سعر البيع' : 'السعر'}</span>
|
||||
<span>الموقع والصور</span>
|
||||
</div>
|
||||
</div>
|
||||
@ -927,7 +945,53 @@ const handleMapClick = async (coords) => {
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{step === 3 && (
|
||||
{step === 3 && purpose === 'sale' && (
|
||||
<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-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.salePrice || ''}
|
||||
onChange={(e) => setFormData({...formData, salePrice: 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.salePrice ? 'border-red-500' : 'border-gray-300'
|
||||
}`}
|
||||
placeholder="مثال: 50000000"
|
||||
/>
|
||||
</div>
|
||||
{errors.salePrice && <p className="text-red-500 text-sm mt-1">{errors.salePrice}</p>}
|
||||
</div>
|
||||
|
||||
<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>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{step === 3 && purpose === 'rent' && (
|
||||
<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">
|
||||
|
||||
Reference in New Issue
Block a user