Fix add property page to match Flutter request body structure
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:
Claw AI
2026-03-29 15:48:48 +00:00
parent 5d7b3e3b0f
commit 00dab824c3
2 changed files with 118 additions and 124 deletions

View File

@ -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,7 +933,6 @@ 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>
@ -932,14 +942,13 @@ function MapClickHandler({ onMapClick }) {
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}`}
{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>
)}

View File

@ -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() {