From 00dab824c30a0583d902f93440aa17ded25959ce Mon Sep 17 00:00:00 2001 From: Claw AI Date: Sun, 29 Mar 2026 15:48:48 +0000 Subject: [PATCH] Fix add property page to match Flutter request body structure - 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 --- app/owner/properties/add/page.js | 226 ++++++++++++++----------------- app/utils/api.js | 16 +++ 2 files changed, 118 insertions(+), 124 deletions(-) diff --git a/app/owner/properties/add/page.js b/app/owner/properties/add/page.js index bfc1923..704a22d 100644 --- a/app/owner/properties/add/page.js +++ b/app/owner/properties/add/page.js @@ -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 (
@@ -922,24 +933,22 @@ function MapClickHandler({ onMapClick }) {
{/* Currency dropdown */} - {currencies.length > 0 && ( -
- - -
- )} +
+ + +
{/* Deposit field */}
@@ -1020,37 +1029,6 @@ function MapClickHandler({ onMapClick }) {
)} - - {formData.offerType === 'sale' && ( - -
- -
- - 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="أدخل السعر المطلوب" - /> -
- {errors.salePrice && ( -

{errors.salePrice}

- )} -
-
- )} )} diff --git a/app/utils/api.js b/app/utils/api.js index f411921..cffe290 100644 --- a/app/utils/api.js +++ b/app/utils/api.js @@ -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} + */ +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() {