diff --git a/frontend/package.json b/frontend/package.json
index 3f6aacd..d3a96de 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -14,6 +14,7 @@
"axios": "^1.6.2",
"leaflet": "^1.9.4",
"react-leaflet": "^4.2.1",
+ "leaflet-draw": "^1.0.4",
"@headlessui/react": "^1.7.17",
"@heroicons/react": "^2.0.18",
"react-hook-form": "^7.48.2",
diff --git a/frontend/src/components/Properties/PropertyForm.js b/frontend/src/components/Properties/PropertyForm.js
new file mode 100644
index 0000000..55fe4bf
--- /dev/null
+++ b/frontend/src/components/Properties/PropertyForm.js
@@ -0,0 +1,305 @@
+import React, { useState, useEffect, useRef } from 'react';
+import { MapContainer, TileLayer, Marker, Popup, useMapEvents } from 'react-leaflet';
+import { Icon } from 'leaflet';
+import 'leaflet/dist/leaflet.css';
+import toast from 'react-hot-toast';
+
+// Fix for default markers in react-leaflet
+delete Icon.Default.prototype._getIconUrl;
+Icon.Default.mergeOptions({
+ iconRetinaUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon-2x.png',
+ iconUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon.png',
+ shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-shadow.png',
+});
+
+// Component to handle map clicks and update marker position
+function LocationMarker({ position, setPosition }) {
+ const map = useMapEvents({
+ click(e) {
+ setPosition([e.latlng.lat, e.latlng.lng]);
+ },
+ });
+
+ return position === null ? null : (
+
+
+ Property Location
+
+ Drag the map or click to adjust the pin position
+
+ Lat: {position[0].toFixed(6)}, Lng: {position[1].toFixed(6)}
+
+
+ );
+}
+
+const PropertyForm = ({ onSubmit, onCancel, initialData = null }) => {
+ const [formData, setFormData] = useState({
+ name: initialData?.name || '',
+ address: initialData?.address || '',
+ latitude: initialData?.latitude || '',
+ longitude: initialData?.longitude || '',
+ totalArea: initialData?.totalArea || ''
+ });
+
+ const [mapPosition, setMapPosition] = useState(
+ initialData?.latitude && initialData?.longitude
+ ? [initialData.latitude, initialData.longitude]
+ : [40.7128, -74.0060] // Default to NYC
+ );
+ const [markerPosition, setMarkerPosition] = useState(
+ initialData?.latitude && initialData?.longitude
+ ? [initialData.latitude, initialData.longitude]
+ : null
+ );
+ const [showMap, setShowMap] = useState(false);
+ const [addressSuggestions, setAddressSuggestions] = useState([]);
+ const [showSuggestions, setShowSuggestions] = useState(false);
+ const debounceRef = useRef(null);
+
+ // Address autocomplete using Nominatim (OpenStreetMap)
+ const searchAddresses = async (query) => {
+ if (query.length < 3) {
+ setAddressSuggestions([]);
+ setShowSuggestions(false);
+ return;
+ }
+
+ try {
+ const response = await fetch(
+ `https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(query)}&limit=5&countrycodes=us`
+ );
+ const data = await response.json();
+
+ const suggestions = data.map(item => ({
+ display_name: item.display_name,
+ lat: parseFloat(item.lat),
+ lon: parseFloat(item.lon)
+ }));
+
+ setAddressSuggestions(suggestions);
+ setShowSuggestions(true);
+ } catch (error) {
+ console.error('Address search failed:', error);
+ }
+ };
+
+ const handleAddressChange = (e) => {
+ const value = e.target.value;
+ setFormData({ ...formData, address: value });
+
+ // Debounce the search
+ if (debounceRef.current) {
+ clearTimeout(debounceRef.current);
+ }
+
+ debounceRef.current = setTimeout(() => {
+ searchAddresses(value);
+ }, 300);
+ };
+
+ const selectAddress = (suggestion) => {
+ setFormData({
+ ...formData,
+ address: suggestion.display_name,
+ latitude: suggestion.lat,
+ longitude: suggestion.lon
+ });
+ setMapPosition([suggestion.lat, suggestion.lon]);
+ setMarkerPosition([suggestion.lat, suggestion.lon]);
+ setShowSuggestions(false);
+ setShowMap(true);
+ };
+
+ const handleShowMap = () => {
+ if (formData.latitude && formData.longitude) {
+ const lat = parseFloat(formData.latitude);
+ const lng = parseFloat(formData.longitude);
+ setMapPosition([lat, lng]);
+ setMarkerPosition([lat, lng]);
+ }
+ setShowMap(true);
+ };
+
+ const updatePositionFromMarker = (position) => {
+ setMarkerPosition(position);
+ setFormData({
+ ...formData,
+ latitude: position[0],
+ longitude: position[1]
+ });
+ };
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+
+ const propertyData = {
+ ...formData,
+ latitude: markerPosition ? markerPosition[0] : (formData.latitude ? parseFloat(formData.latitude) : null),
+ longitude: markerPosition ? markerPosition[1] : (formData.longitude ? parseFloat(formData.longitude) : null),
+ totalArea: formData.totalArea ? parseFloat(formData.totalArea) : null
+ };
+
+ try {
+ await onSubmit(propertyData);
+ } catch (error) {
+ console.error('Form submission failed:', error);
+ }
+ };
+
+ return (
+
+
+ {initialData ? 'Edit Property' : 'Add New Property'}
+
+
+
+
+ );
+};
+
+export default PropertyForm;
\ No newline at end of file
diff --git a/frontend/src/pages/Properties/Properties.js b/frontend/src/pages/Properties/Properties.js
index b03773a..3ef7a19 100644
--- a/frontend/src/pages/Properties/Properties.js
+++ b/frontend/src/pages/Properties/Properties.js
@@ -3,19 +3,13 @@ import { Link } from 'react-router-dom';
import { PlusIcon, MapPinIcon, TrashIcon, PencilIcon } from '@heroicons/react/24/outline';
import { propertiesAPI } from '../../services/api';
import LoadingSpinner from '../../components/UI/LoadingSpinner';
+import PropertyForm from '../../components/Properties/PropertyForm';
import toast from 'react-hot-toast';
const Properties = () => {
const [properties, setProperties] = useState([]);
const [loading, setLoading] = useState(true);
const [showCreateForm, setShowCreateForm] = useState(false);
- const [formData, setFormData] = useState({
- name: '',
- address: '',
- latitude: '',
- longitude: '',
- totalArea: ''
- });
useEffect(() => {
fetchProperties();
@@ -45,24 +39,16 @@ const Properties = () => {
}
};
- const handleSubmit = async (e) => {
- e.preventDefault();
+ const handleCreateProperty = async (propertyData) => {
try {
- const propertyData = {
- ...formData,
- latitude: formData.latitude ? parseFloat(formData.latitude) : null,
- longitude: formData.longitude ? parseFloat(formData.longitude) : null,
- totalArea: formData.totalArea ? parseFloat(formData.totalArea) : null
- };
-
await propertiesAPI.create(propertyData);
toast.success('Property created successfully!');
setShowCreateForm(false);
- setFormData({ name: '', address: '', latitude: '', longitude: '', totalArea: '' });
fetchProperties();
} catch (error) {
console.error('Failed to create property:', error);
toast.error(error.response?.data?.message || 'Failed to create property');
+ throw error; // Re-throw to let the form handle it
}
};
@@ -108,82 +94,10 @@ const Properties = () => {
{/* Create Property Form */}
{showCreateForm && (
-
+ setShowCreateForm(false)}
+ />
)}
{/* Properties List */}