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 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,24 +933,22 @@ 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> </label>
</label> <select
<select value={selectedCurrencyId}
value={selectedCurrencyId} 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" >
> {Object.entries(CurrencyLabels).map(([id, label]) => (
{currencies.map((c) => ( <option key={id} value={id}>
<option key={c.id} value={c.id}> {label}
{c.name || c.sign || `Currency ${c.id}`} </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>
)} )}

View File

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