Fix add property page to match Flutter request body structure
Some checks failed
Build frontend / build (push) Failing after 39s
Some checks failed
Build frontend / build (push) Failing after 39s
- Remove 'For sale' offer type (rent only) - Remove salePrice field and UI - Fix rentTypeMap: 0=Monthly, 1=Daily (was wrong) - Fix propertyType: uses RentPropertyCondition (furnished/unfurnished) - Fix type field: uses RentPropertyType (furnished/unfurnished) - Fix services: use enum API names in detailsJSON (Electricity, Internet...) - Fix terms: use enum API names in detailsJSON (NoSmoking, NoAnimals...) - Fix detailsJSON structure to match Flutter (services array, terms array, room object) - Replace getCurrencies with static Currency enum dropdown - Remove duplicate MapClickHandler - Use all new enums from enums/index.js
This commit is contained in:
@ -51,7 +51,21 @@ import {
|
|||||||
Move
|
Move
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import toast, { Toaster } from 'react-hot-toast';
|
import toast, { Toaster } from 'react-hot-toast';
|
||||||
import { addRentProperty, getCurrencies } from '../../../utils/api';
|
import { addRentProperty } from '../../../utils/api';
|
||||||
|
import {
|
||||||
|
BuildingType,
|
||||||
|
RentPropertyCondition,
|
||||||
|
RentPropertyType,
|
||||||
|
RentType,
|
||||||
|
PropertyService,
|
||||||
|
PropertyServiceLabels,
|
||||||
|
PropertyServicesList,
|
||||||
|
PropertyTerm,
|
||||||
|
PropertyTermLabels,
|
||||||
|
PropertyTermsList,
|
||||||
|
Currency,
|
||||||
|
CurrencyLabels
|
||||||
|
} from '../../../enums';
|
||||||
|
|
||||||
const MapContainer = dynamic(() => import('react-leaflet').then(mod => mod.MapContainer), { ssr: false });
|
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 TileLayer = dynamic(() => import('react-leaflet').then(mod => mod.TileLayer), { ssr: false });
|
||||||
@ -107,7 +121,6 @@ export default function AddPropertyPage() {
|
|||||||
|
|
||||||
dailyPrice: '',
|
dailyPrice: '',
|
||||||
monthlyPrice: '',
|
monthlyPrice: '',
|
||||||
salePrice: '',
|
|
||||||
|
|
||||||
city: '',
|
city: '',
|
||||||
district: '',
|
district: '',
|
||||||
@ -127,8 +140,7 @@ export default function AddPropertyPage() {
|
|||||||
const [mapZoom, setMapZoom] = useState(13);
|
const [mapZoom, setMapZoom] = useState(13);
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
const [mapLoaded, setMapLoaded] = useState(false);
|
const [mapLoaded, setMapLoaded] = useState(false);
|
||||||
const [currencies, setCurrencies] = useState([]);
|
const [selectedCurrencyId, setSelectedCurrencyId] = useState(Currency.SYP);
|
||||||
const [selectedCurrencyId, setSelectedCurrencyId] = useState(1);
|
|
||||||
|
|
||||||
const [errors, setErrors] = useState({});
|
const [errors, setErrors] = useState({});
|
||||||
|
|
||||||
@ -144,29 +156,25 @@ export default function AddPropertyPage() {
|
|||||||
];
|
];
|
||||||
|
|
||||||
const serviceList = [
|
const serviceList = [
|
||||||
{ id: 'electricity', label: 'كهرباء', icon: Zap },
|
{ id: PropertyService.ELECTRICITY, label: PropertyServiceLabels[PropertyService.ELECTRICITY], icon: Zap },
|
||||||
{ id: 'internet', label: 'انترنت', icon: Wifi },
|
{ id: PropertyService.INTERNET, label: PropertyServiceLabels[PropertyService.INTERNET], icon: Wifi },
|
||||||
{ id: 'heating', label: 'تدفئة', icon: Flame },
|
{ id: PropertyService.HEATING, label: PropertyServiceLabels[PropertyService.HEATING], icon: Flame },
|
||||||
{ id: 'water', label: 'ماء', icon: Droplets },
|
{ id: PropertyService.WATER, label: PropertyServiceLabels[PropertyService.WATER], icon: Droplets },
|
||||||
{ id: 'airConditioning', label: 'تكييف', icon: Wind },
|
{ id: PropertyService.CENTRAL_AIR_CONDITIONING, label: PropertyServiceLabels[PropertyService.CENTRAL_AIR_CONDITIONING], icon: Wind },
|
||||||
{ id: 'parking', label: 'موقف سيارات', icon: Warehouse },
|
{ id: PropertyService.PARKING, label: PropertyServiceLabels[PropertyService.PARKING], icon: Warehouse },
|
||||||
{ id: 'elevator', label: 'مصعد', icon: Layers }
|
{ id: PropertyService.ELEVATOR, label: PropertyServiceLabels[PropertyService.ELEVATOR], icon: Layers },
|
||||||
];
|
];
|
||||||
|
|
||||||
const termsList = [
|
const termsList = [
|
||||||
{ id: 'noSmoking', label: 'ممنوع التدخين', icon: Cigarette },
|
{ id: PropertyTerm.NO_SMOKING, label: PropertyTermLabels[PropertyTerm.NO_SMOKING], icon: Cigarette },
|
||||||
{ id: 'noPets', label: 'ممنوع الحيوانات', icon: Dog },
|
{ id: PropertyTerm.NO_ANIMALS, label: PropertyTermLabels[PropertyTerm.NO_ANIMALS], icon: Dog },
|
||||||
{ id: 'noParties', label: 'عدم إقامة حفلات', icon: Music },
|
{ id: PropertyTerm.NO_PARTIES, label: PropertyTermLabels[PropertyTerm.NO_PARTIES], icon: Music },
|
||||||
{ id: 'noAlcohol', label: 'ممنوع الكحول', icon: X },
|
|
||||||
{ id: 'suitableForChildren', label: 'مناسب للأطفال', icon: Star },
|
|
||||||
{ id: 'suitableForElderly', label: 'مناسب لكبار السن', icon: Star }
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const offerTypes = [
|
const offerTypes = [
|
||||||
{ id: 'daily', label: 'إيجار يومي', icon: Clock },
|
{ id: 'daily', label: 'إيجار يومي', icon: Clock },
|
||||||
{ id: 'monthly', label: 'إيجار شهري', icon: Calendar },
|
{ id: 'monthly', label: 'إيجار شهري', icon: Calendar },
|
||||||
{ id: 'both', label: 'إيجار يومي وشهري', icon: Calendar },
|
{ id: 'both', label: 'إيجار يومي وشهري', icon: Calendar },
|
||||||
{ id: 'sale', label: 'للبيع', icon: DollarSign }
|
|
||||||
];
|
];
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -180,17 +188,6 @@ export default function AddPropertyPage() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
setMapLoaded(true);
|
setMapLoaded(true);
|
||||||
|
|
||||||
// Fetch available currencies
|
|
||||||
getCurrencies().then((data) => {
|
|
||||||
if (Array.isArray(data) && data.length > 0) {
|
|
||||||
setCurrencies(data);
|
|
||||||
setSelectedCurrencyId(data[0].id);
|
|
||||||
console.log('[AddProperty] Currencies loaded:', data);
|
|
||||||
}
|
|
||||||
}).catch((err) => {
|
|
||||||
console.warn('[AddProperty] Failed to load currencies:', err);
|
|
||||||
});
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleSearch = async () => {
|
const handleSearch = async () => {
|
||||||
@ -479,9 +476,6 @@ const handleMapClick = async (coords) => {
|
|||||||
if (!formData.dailyPrice) newErrors.dailyPrice = 'السعر اليومي مطلوب';
|
if (!formData.dailyPrice) newErrors.dailyPrice = 'السعر اليومي مطلوب';
|
||||||
if (!formData.monthlyPrice) newErrors.monthlyPrice = 'السعر الشهري مطلوب';
|
if (!formData.monthlyPrice) newErrors.monthlyPrice = 'السعر الشهري مطلوب';
|
||||||
}
|
}
|
||||||
if (formData.offerType === 'sale' && !formData.salePrice) {
|
|
||||||
newErrors.salePrice = 'سعر البيع مطلوب';
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 4:
|
case 4:
|
||||||
@ -516,46 +510,72 @@ const handleMapClick = async (coords) => {
|
|||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
console.log('[AddProperty] Building RentPropertyDto payload...');
|
console.log('[AddProperty] Building RentPropertyDto payload...');
|
||||||
|
|
||||||
// Map UI BuildingType to API enum: 0=Apartment, 1=Villa, 2=House
|
// Map UI property type to API BuildingType enum
|
||||||
const buildingTypeMap = { apartment: 0, villa: 1, suite: 0, room: 0 };
|
const buildingTypeMap = { apartment: BuildingType.APARTMENT, villa: BuildingType.VILLA, suite: BuildingType.APARTMENT, room: BuildingType.APARTMENT };
|
||||||
// Map UI offerType to API RentType: 0=day, 1=week, 2=month
|
|
||||||
const rentTypeMap = { daily: 0, monthly: 2, both: 2, sale: 2 };
|
|
||||||
// Map UI propertyType to API RentPropertyType: 0=Family, 1=Person
|
|
||||||
const rentPropertyType = formData.terms?.suitableForChildren ? 0 : 1;
|
|
||||||
|
|
||||||
// Services/terms go into DetailsJSON
|
// 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")
|
||||||
|
|
||||||
|
// 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
|
||||||
const detailsJSON = JSON.stringify({
|
const detailsJSON = JSON.stringify({
|
||||||
services: formData.services,
|
services: selectedServices,
|
||||||
terms: formData.terms,
|
serviceDetails: selectedServices.reduce((acc, s) => ({ ...acc, [s]: 'in general' }), {}),
|
||||||
furnished: formData.furnished,
|
terms: selectedTerms,
|
||||||
livingRooms: formData.livingRooms,
|
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: '',
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
PropertyInformation: {
|
propertyInformation: {
|
||||||
CordsX: formData.lat ? String(formData.lat) : '',
|
cordsX: formData.lat ? String(formData.lat) : '',
|
||||||
CordsY: formData.lng ? String(formData.lng) : '',
|
cordsY: formData.lng ? String(formData.lng) : '',
|
||||||
Address: `${formData.city} - ${formData.district} - ${formData.address}`.trim(),
|
address: `${formData.city} - ${formData.district} - ${formData.address}`.trim(),
|
||||||
Description: formData.description || '',
|
description: formData.description || '',
|
||||||
NumberOfBathRooms: formData.bathrooms || 0,
|
numberOfBathRooms: formData.bathrooms || 0,
|
||||||
NumberOfRooms: (formData.bedrooms || 0) + (formData.livingRooms || 0),
|
numberOfRooms: (formData.bedrooms || 0) + (formData.livingRooms || 0),
|
||||||
NumberOfBedRooms: formData.bedrooms || 0,
|
numberOfBedRooms: formData.bedrooms || 0,
|
||||||
Space: parseFloat(formData.space) || 0,
|
space: parseFloat(formData.space) || 0,
|
||||||
DetailsJSON: detailsJSON,
|
detailsJSON,
|
||||||
BuildingType: buildingTypeMap[formData.propertyType] ?? 0,
|
buildingType: buildingTypeMap[formData.propertyType] ?? BuildingType.APARTMENT,
|
||||||
Status: 0,
|
status: 0,
|
||||||
PropertyType: formData.offerType === 'sale' ? 1 : 0,
|
propertyType: formData.furnished ? RentPropertyCondition.WITH_FURNITURE : RentPropertyCondition.WITHOUT_FURNITURE,
|
||||||
},
|
},
|
||||||
Deposit: parseFloat(formData.deposit) || 0,
|
deposit: parseFloat(formData.deposit) || 0,
|
||||||
MonthlyRent: parseFloat(formData.monthlyPrice) || 0,
|
monthlyRent: parseFloat(formData.monthlyPrice) || 0,
|
||||||
DailyRent: parseFloat(formData.dailyPrice) || 0,
|
dailyRent: parseFloat(formData.dailyPrice) || 0,
|
||||||
Rating: 0,
|
rating: 0,
|
||||||
CurrencyId: selectedCurrencyId,
|
currencyId: selectedCurrencyId,
|
||||||
RentType: rentTypeMap[formData.offerType] ?? 0,
|
rentType: rentTypeMap[formData.offerType] ?? RentType.MONTHLY,
|
||||||
IsSmokeAllow: !formData.terms?.noSmoking,
|
isSmokeAllow: !formData.terms[PropertyTerm.NO_SMOKING],
|
||||||
SpecializedFor: false,
|
specializedFor: false,
|
||||||
IsVisitorAllow: !formData.terms?.noParties,
|
isVisitorAllow: !formData.terms[PropertyTerm.NO_PARTIES],
|
||||||
Type: rentPropertyType,
|
type: formData.furnished ? RentPropertyType.FURNISHED : RentPropertyType.UNFURNISHED,
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log('[AddProperty] Payload:', JSON.stringify(payload, null, 2));
|
console.log('[AddProperty] Payload:', JSON.stringify(payload, null, 2));
|
||||||
@ -581,15 +601,6 @@ const handleMapClick = async (coords) => {
|
|||||||
transition: { duration: 0.5 }
|
transition: { duration: 0.5 }
|
||||||
};
|
};
|
||||||
|
|
||||||
function MapClickHandler({ onMapClick }) {
|
|
||||||
const map = useMapEvents({
|
|
||||||
dblclick: (e) => {
|
|
||||||
const { lat, lng } = e.latlng;
|
|
||||||
onMapClick([lat, lng]);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50 py-8">
|
<div className="min-h-screen bg-gray-50 py-8">
|
||||||
<Toaster position="top-center" reverseOrder={false} />
|
<Toaster position="top-center" reverseOrder={false} />
|
||||||
@ -922,7 +933,6 @@ function MapClickHandler({ onMapClick }) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Currency dropdown */}
|
{/* Currency dropdown */}
|
||||||
{currencies.length > 0 && (
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
العملة <span className="text-red-500">*</span>
|
العملة <span className="text-red-500">*</span>
|
||||||
@ -932,14 +942,13 @@ function MapClickHandler({ onMapClick }) {
|
|||||||
onChange={(e) => setSelectedCurrencyId(parseInt(e.target.value))}
|
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"
|
className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500"
|
||||||
>
|
>
|
||||||
{currencies.map((c) => (
|
{Object.entries(CurrencyLabels).map(([id, label]) => (
|
||||||
<option key={c.id} value={c.id}>
|
<option key={id} value={id}>
|
||||||
{c.name || c.sign || `Currency ${c.id}`}
|
{label}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Deposit field */}
|
{/* Deposit field */}
|
||||||
<div>
|
<div>
|
||||||
@ -1020,37 +1029,6 @@ function MapClickHandler({ onMapClick }) {
|
|||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{formData.offerType === 'sale' && (
|
|
||||||
<motion.div
|
|
||||||
key="sale"
|
|
||||||
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.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="أدخل السعر المطلوب"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{errors.salePrice && (
|
|
||||||
<p className="text-red-500 text-sm mt-1">{errors.salePrice}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -116,6 +116,22 @@ export async function getProperty(id) {
|
|||||||
return apiFetch(`/Properties/Get/${id}`);
|
return apiFetch(`/Properties/Get/${id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── Rent Properties (Add) ───
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a rent property
|
||||||
|
* Request body must match Flutter AddRentPropertyDto exactly
|
||||||
|
* @param {object} data — structured request body
|
||||||
|
* @returns {Promise<object>}
|
||||||
|
*/
|
||||||
|
export async function addRentProperty(data) {
|
||||||
|
console.log('[API] Adding rent property...');
|
||||||
|
return apiFetch('/RentProperties/AddRentProperty', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// ─── Recommendations ───
|
// ─── Recommendations ───
|
||||||
|
|
||||||
export async function getRecommendations() {
|
export async function getRecommendations() {
|
||||||
|
|||||||
Reference in New Issue
Block a user