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
|
||||
} from 'lucide-react';
|
||||
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 TileLayer = dynamic(() => import('react-leaflet').then(mod => mod.TileLayer), { ssr: false });
|
||||
@ -107,7 +121,6 @@ export default function AddPropertyPage() {
|
||||
|
||||
dailyPrice: '',
|
||||
monthlyPrice: '',
|
||||
salePrice: '',
|
||||
|
||||
city: '',
|
||||
district: '',
|
||||
@ -127,8 +140,7 @@ export default function AddPropertyPage() {
|
||||
const [mapZoom, setMapZoom] = useState(13);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [mapLoaded, setMapLoaded] = useState(false);
|
||||
const [currencies, setCurrencies] = useState([]);
|
||||
const [selectedCurrencyId, setSelectedCurrencyId] = useState(1);
|
||||
const [selectedCurrencyId, setSelectedCurrencyId] = useState(Currency.SYP);
|
||||
|
||||
const [errors, setErrors] = useState({});
|
||||
|
||||
@ -144,29 +156,25 @@ export default function AddPropertyPage() {
|
||||
];
|
||||
|
||||
const serviceList = [
|
||||
{ id: 'electricity', label: 'كهرباء', icon: Zap },
|
||||
{ id: 'internet', label: 'انترنت', icon: Wifi },
|
||||
{ id: 'heating', label: 'تدفئة', icon: Flame },
|
||||
{ id: 'water', label: 'ماء', icon: Droplets },
|
||||
{ id: 'airConditioning', label: 'تكييف', icon: Wind },
|
||||
{ id: 'parking', label: 'موقف سيارات', icon: Warehouse },
|
||||
{ id: 'elevator', label: 'مصعد', icon: Layers }
|
||||
{ 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 },
|
||||
];
|
||||
|
||||
const termsList = [
|
||||
{ id: 'noSmoking', label: 'ممنوع التدخين', icon: Cigarette },
|
||||
{ id: 'noPets', label: 'ممنوع الحيوانات', icon: Dog },
|
||||
{ id: 'noParties', label: 'عدم إقامة حفلات', icon: Music },
|
||||
{ id: 'noAlcohol', label: 'ممنوع الكحول', icon: X },
|
||||
{ id: 'suitableForChildren', label: 'مناسب للأطفال', icon: Star },
|
||||
{ id: 'suitableForElderly', label: 'مناسب لكبار السن', icon: Star }
|
||||
{ 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 },
|
||||
];
|
||||
|
||||
const offerTypes = [
|
||||
{ id: 'daily', label: 'إيجار يومي', icon: Clock },
|
||||
{ id: 'monthly', label: 'إيجار شهري', icon: Calendar },
|
||||
{ id: 'both', label: 'إيجار يومي وشهري', icon: Calendar },
|
||||
{ id: 'sale', label: 'للبيع', icon: DollarSign }
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
@ -180,17 +188,6 @@ export default function AddPropertyPage() {
|
||||
});
|
||||
}
|
||||
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 () => {
|
||||
@ -479,9 +476,6 @@ const handleMapClick = async (coords) => {
|
||||
if (!formData.dailyPrice) newErrors.dailyPrice = 'السعر اليومي مطلوب';
|
||||
if (!formData.monthlyPrice) newErrors.monthlyPrice = 'السعر الشهري مطلوب';
|
||||
}
|
||||
if (formData.offerType === 'sale' && !formData.salePrice) {
|
||||
newErrors.salePrice = 'سعر البيع مطلوب';
|
||||
}
|
||||
break;
|
||||
|
||||
case 4:
|
||||
@ -516,46 +510,72 @@ const handleMapClick = async (coords) => {
|
||||
setIsLoading(true);
|
||||
console.log('[AddProperty] Building RentPropertyDto payload...');
|
||||
|
||||
// Map UI BuildingType to API enum: 0=Apartment, 1=Villa, 2=House
|
||||
const buildingTypeMap = { apartment: 0, villa: 1, suite: 0, room: 0 };
|
||||
// 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;
|
||||
// Map UI property type to API BuildingType enum
|
||||
const buildingTypeMap = { apartment: BuildingType.APARTMENT, villa: BuildingType.VILLA, suite: BuildingType.APARTMENT, room: BuildingType.APARTMENT };
|
||||
|
||||
// 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({
|
||||
services: formData.services,
|
||||
terms: formData.terms,
|
||||
furnished: formData.furnished,
|
||||
livingRooms: formData.livingRooms,
|
||||
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: '',
|
||||
}
|
||||
});
|
||||
|
||||
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: detailsJSON,
|
||||
BuildingType: buildingTypeMap[formData.propertyType] ?? 0,
|
||||
Status: 0,
|
||||
PropertyType: formData.offerType === 'sale' ? 1 : 0,
|
||||
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,
|
||||
},
|
||||
Deposit: parseFloat(formData.deposit) || 0,
|
||||
MonthlyRent: parseFloat(formData.monthlyPrice) || 0,
|
||||
DailyRent: parseFloat(formData.dailyPrice) || 0,
|
||||
Rating: 0,
|
||||
CurrencyId: selectedCurrencyId,
|
||||
RentType: rentTypeMap[formData.offerType] ?? 0,
|
||||
IsSmokeAllow: !formData.terms?.noSmoking,
|
||||
SpecializedFor: false,
|
||||
IsVisitorAllow: !formData.terms?.noParties,
|
||||
Type: rentPropertyType,
|
||||
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] Payload:', JSON.stringify(payload, null, 2));
|
||||
@ -581,15 +601,6 @@ const handleMapClick = async (coords) => {
|
||||
transition: { duration: 0.5 }
|
||||
};
|
||||
|
||||
function MapClickHandler({ onMapClick }) {
|
||||
const map = useMapEvents({
|
||||
dblclick: (e) => {
|
||||
const { lat, lng } = e.latlng;
|
||||
onMapClick([lat, lng]);
|
||||
},
|
||||
});
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 py-8">
|
||||
<Toaster position="top-center" reverseOrder={false} />
|
||||
@ -922,24 +933,22 @@ function MapClickHandler({ onMapClick }) {
|
||||
</div>
|
||||
|
||||
{/* Currency dropdown */}
|
||||
{currencies.length > 0 && (
|
||||
<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"
|
||||
>
|
||||
{currencies.map((c) => (
|
||||
<option key={c.id} value={c.id}>
|
||||
{c.name || c.sign || `Currency ${c.id}`}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</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>
|
||||
|
||||
{/* Deposit field */}
|
||||
<div>
|
||||
@ -1020,37 +1029,6 @@ function MapClickHandler({ onMapClick }) {
|
||||
</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>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
@ -116,6 +116,22 @@ export async function getProperty(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 ───
|
||||
|
||||
export async function getRecommendations() {
|
||||
|
||||
Reference in New Issue
Block a user