import React, { useState, useMemo, useEffect } from 'react';
import { MapContainer, TileLayer, Polygon, useMapEvents, Marker, Popup } from 'react-leaflet';
import { X, Home, DollarSign, User, Info, Edit2, Save, Check, MapPin, Loader2, Plus, AlertCircle } from 'lucide-react';
import 'leaflet/dist/leaflet.css';
import { useMockStore } from '../data/mockStore';
import { useAuth } from '../context/AuthContext';
import { SpotlightCard } from '../components/SpotlightCard';
import { useTheme } from '../context/ThemeContext';
import { logger } from '../utils/logger';
import { toast } from 'sonner';
import Loader from '../components/Loader';
// Fix for default Leaflet marker icons not showing up
import L from 'leaflet';
import icon from 'leaflet/dist/images/marker-icon.png';
import iconShadow from 'leaflet/dist/images/marker-shadow.png';
let DefaultIcon = L.icon({
iconUrl: icon,
shadowUrl: iconShadow,
iconSize: [25, 41],
iconAnchor: [12, 41]
});
L.Marker.prototype.options.icon = DefaultIcon;
// --- COMPONENTS ---
const StatusBadge = ({ status }) => {
const colors = {
"Neutral": "bg-violet-400",
"Hot Lead": "bg-red-500",
"Renovated": "bg-blue-500",
"Customer": "bg-emerald-500",
"Not Interested": "bg-black"
};
return (
{status}
);
};
// Map Interaction Component
const MapInteraction = ({ onMapClick }) => {
useMapEvents({
click(e) {
onMapClick(e.latlng);
},
});
return null;
};
const MapLegend = () => (
);
// Map View
const MapView = ({ data, onSelect, onMapCreate }) => {
const mapStart = [33.0708, -96.7455];
const { theme } = useTheme(); // Get current theme
return (
{/*
- Light Mode: Standard vibrant OSM tiles (no filters)
- Dark Mode: Inverted, Grayscale, High Contrast for dark map
- Key prop forces remount on theme change to ensure tiles update immediately
*/}
{data.map((item) => {
let color = "#a78bfa"; // Violet-400 (New Neutral)
let fillOpacity = 0.2;
switch (item.canvassingStatus) {
case "Hot Lead":
color = "#ef4444"; // Red-500
fillOpacity = 0.4;
break;
case "Customer":
color = "#10b981"; // Emerald-500
fillOpacity = 0.3;
break;
case "Renovated":
color = "#3b82f6"; // Blue-500
break;
case "Not Interested":
color = "#18181b"; // Zinc-900 (Blackish)
fillOpacity = 0.1;
break;
default:
// Neutral (Violet)
break;
}
return (
{
L.DomEvent.stopPropagation(e); // Prevent map click
onSelect(item);
},
}}
/>
);
})}
);
};
const Drawer = ({ isOpen, onClose, data, newLocation, onUpdateStatus, onSave, loadingAddr }) => {
const [isEditing, setIsEditing] = useState(false);
// Initial State Matching Schema
const initialFormState = {
// Status
status: 'Neutral',
// 1.1 Basic Property Details
propertyType: 'House',
builtUpArea: '',
lotSize: '',
yearBuilt: '',
bedrooms: '',
bathrooms: '',
parkingSpaces: '',
// 1.2 Transaction & Market
purchaseDate: '',
purchasePrice: '',
marketValue: 0,
taxValue: '',
// 1.3 Renovation
roofCondition: 'Good',
lastRenovationDate: '',
// 1.4 Rental Status
isRented: 'No', // Yes/No
tenantName: '',
currentRent: '',
leaseEnd: '',
ownerWillingToRent: 'Yes',
expectedRent: '',
// 1.5 Location
schoolDistrict: '',
neighborhoodRating: 5,
// 2.1 Owner Details
ownerName: '',
ownerPhone: '',
ownerEmail: '',
ownerAddress: '',
occupation: '',
ownershipType: 'Individual',
// 2.4 Owner Intent
willingToSell: 'No',
desiredPrice: '',
minPrice: '',
// Notes
notes: ''
};
const [formData, setFormData] = useState(initialFormState);
// Reset or Initialize Form
useEffect(() => {
if (data) {
// Mapping existing data to form state (handling potentially missing fields safely)
setFormData({
...initialFormState,
status: data.canvassingStatus || 'Neutral',
ownerName: data.ownerData?.fullName || '',
ownerPhone: data.ownerData?.primaryPhoneNumber || '',
ownerEmail: data.ownerData?.emailAddress || '',
marketValue: data.propertyData?.currentEstimatedMarketValue || 0,
notes: data.propertyData?.notes || '',
propertyType: data.propertyData?.propertyType || 'House',
// Correctly map fields from mockStore structure (flat objects) to form state
builtUpArea: data.propertyData?.totalBuiltUpArea || '',
lotSize: data.propertyData?.lotPlotSize || '', // mockStore might return string "0.22 Acres", assuming input handles text or needs parsing if strictly number
yearBuilt: data.propertyData?.yearBuilt || '',
bedrooms: data.propertyData?.numberOfBedrooms || '',
bathrooms: data.propertyData?.numberOfBathrooms || '',
parkingSpaces: data.propertyData?.numberOfParkingSpaces || '',
taxValue: data.propertyData?.propertyTaxAssessmentValue || '',
roofCondition: data.propertyData?.roofCondition || 'Good',
lastRenovationDate: data.propertyData?.lastMajorRenovationDate || '',
isRented: data.propertyData?.currentlyRented ? 'Yes' : 'No',
tenantName: data.propertyData?.currentTenantName || '',
currentRent: data.propertyData?.currentMonthlyRentAmount || '',
leaseEnd: data.propertyData?.leaseEndDate || '',
ownerWillingToRent: data.propertyData?.ownerWillingToRent ? 'Yes' : 'No',
expectedRent: data.propertyData?.expectedMonthlyRentAmount || '',
schoolDistrict: data.propertyData?.schoolDistrict || '',
neighborhoodRating: data.propertyData?.neighborhoodRating || 5,
occupation: data.ownerData?.occupation || '',
ownershipType: data.ownerData?.ownershipType || 'Individual',
willingToSell: data.ownerData?.willingToSellProperty ? 'Yes' : 'No',
desiredPrice: data.ownerData?.desiredSellingPrice || '',
minPrice: data.ownerData?.minimumAcceptableSellingPrice || '',
ownerAddress: data.ownerData?.mailingAddress || '',
});
setIsEditing(false);
} else if (newLocation) {
setFormData(initialFormState);
setIsEditing(true);
}
}, [data, newLocation]);
const handleSave = () => {
onSave(formData);
if (data) setIsEditing(false);
};
if (!isOpen) return null;
const isCreating = !data && !!newLocation;
const address = data?.propertyData?.propertyAddress || newLocation?.address || "Unknown Location";
const coords = data ? null : newLocation;
// Helper for Inputs
const RenderInput = ({ label, field, type = "text", placeholder, options, locked = false }) => {
if (!isEditing) {
return (
{formData[field] || "-"}
);
}
if (locked) {
return (
{placeholder || "Not Applicable"}
);
}
return (
{options ? (
) : (
setFormData({ ...formData, [field]: e.target.value })}
placeholder={placeholder}
className="w-full bg-zinc-50 dark:bg-black/20 border border-zinc-200 dark:border-white/10 rounded-lg py-2 px-3 text-sm text-zinc-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-blue-500 placeholder-zinc-400"
/>
)}
);
};
return (
{/* Header */}
{/* Pattern */}
{isCreating ? New Entry : }
{loadingAddr ? "Fetching Address..." : address.split(',')[0]}
{loadingAddr ? "Locating..." : address}
{/* Body Content */}
{loadingAddr ? (
Resolving Location...
) : (
<>
{/* Status Selector */}
{isEditing && (
{["Neutral", "Hot Lead", "Customer", "Not Interested"].map(s => (
))}
)}
{/* SECTION 1: BASIC PROPERTY INFO */}
{/* SECTION 2: VALUES & CONDITION */}
{/* SECTION 3: RENTAL STATUS (Conditional) */}
{formData.isRented === 'Yes' ? (
) : (
{formData.ownerWillingToRent !== 'No' && (
)}
)}
{/* SECTION 4: OWNER INTENT (Conditional) */}
{formData.willingToSell === 'Yes' ? (
) : (
)}
{/* SECTION 5: OWNER CONTACT */}
{/* SECTION 6: NOTES */}
Field Notes
{isEditing ? (
>
)}
{/* Footer */}
{isEditing ? (
) : (
)}
);
};
const InputGroup = ({ label, children }) => (
);
// --- MAIN PAGE COMPONENT --- //
function Maps() {
const { properties, setProperties, updatePropertyStatus } = useMockStore();
const { user } = useAuth();
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
// Simulate initial map load
const timer = setTimeout(() => setIsLoading(false), 1000);
return () => clearTimeout(timer);
}, []);
// Selection State
const [selectedPropertyId, setSelectedPropertyId] = useState(null);
const [newPropertyLocation, setNewPropertyLocation] = useState(null); // { lat, lng, address }
const [loadingAddr, setLoadingAddr] = useState(false);
// Derived Selection
const selectedProperty = useMemo(() =>
properties.find(p => p.id === selectedPropertyId),
[properties, selectedPropertyId]
);
// -- Handlers --
// 1. Click Existing Polygon
const handlePropertySelect = (property) => {
setNewPropertyLocation(null); // Clear new creation mode
setSelectedPropertyId(property.id);
};
// 2. Click Empty Map (Create New)
const handleMapCreate = async (latlng) => {
// Only Field Agents (employees) can create
if (user?.role !== 'FIELD_AGENT' && user?.role !== 'ADMIN') return;
setSelectedPropertyId(null);
setNewPropertyLocation({ lat: latlng.lat, lng: latlng.lng, address: '' });
setLoadingAddr(true);
try {
// Reverse Geocode using simple OSM Nominatim API
// Note: In production, user should cache/throttle or use paid service.
const response = await fetch(`https://nominatim.openstreetmap.org/reverse?format=json&lat=${latlng.lat}&lon=${latlng.lng}`);
if (!response.ok) throw new Error("Geocoding service unavailable");
const data = await response.json();
setNewPropertyLocation(prev => ({
...prev,
address: data.display_name || "Unknown Location"
}));
logger.info("Geocoding successful", { lat: latlng.lat, lng: latlng.lng });
} catch (err) {
logger.error("Geocoding failed", err);
toast.error("Could not fetch address", { description: "Using coordinates instead." });
setNewPropertyLocation(prev => ({ ...prev, address: "Location Found (Address Unavailable)" }));
} finally {
setLoadingAddr(false);
}
};
// 3. Save Changes (Create or Update)
const handleSave = (formData) => {
try {
// Construct the full data objects based on schema
const newOwnerData = {
fullName: formData.ownerName,
primaryPhoneNumber: formData.ownerPhone || "Pending",
emailAddress: formData.ownerEmail || "Pending",
details: {
occupation: formData.occupation,
ownershipType: formData.ownershipType,
willingToSell: formData.willingToSell,
desiredPrice: formData.desiredPrice,
minPrice: formData.minPrice,
ownerAddress: formData.ownerAddress
}
};
const newPropertyData = {
propertyType: formData.propertyType,
currentEstimatedMarketValue: parseInt(formData.marketValue) || 0,
notes: formData.notes,
details: {
builtUpArea: formData.builtUpArea,
lotSize: formData.lotSize,
yearBuilt: formData.yearBuilt,
bedrooms: formData.bedrooms,
bathrooms: formData.bathrooms,
parkingSpaces: formData.parkingSpaces,
taxValue: formData.taxValue,
roofCondition: formData.roofCondition,
lastRenovationDate: formData.lastRenovationDate,
// Rental Status
isRented: formData.isRented,
tenantName: formData.tenantName,
currentRent: formData.currentRent,
leaseEnd: formData.leaseEnd,
ownerWillingToRent: formData.ownerWillingToRent,
expectedRent: formData.expectedRent,
// Location (can be expanded)
schoolDistrict: formData.schoolDistrict,
neighborhoodRating: formData.neighborhoodRating
}
};
if (selectedPropertyId) {
// Update Existing
setProperties(prev => prev.map(p => {
if (p.id === selectedPropertyId) {
return {
...p,
canvassingStatus: formData.status,
ownerData: { ...p.ownerData, ...newOwnerData, details: { ...p.ownerData.details, ...newOwnerData.details } },
propertyData: {
...p.propertyData,
...newPropertyData,
details: { ...p.propertyData.details, ...newPropertyData.details } // Merge to keep existing data not in form if any
}
};
}
return p;
}));
} else if (newPropertyLocation) {
// Create New
const newId = `prop-${Date.now()}`;
const newProp = {
id: newId,
polygon: [], // We don't have a polygon drawing tool yet, implies point-based or empty
canvassingStatus: formData.status,
ownerData: newOwnerData,
propertyData: {
...newPropertyData,
propertyAddress: newPropertyLocation.address,
}
};
// In a real app we'd need to store the LatLng for this new valid prop separately if it has no polygon
// For now, we just add it to the list.
// NOTE: Since MapsView maps *Polygons*, this new item won't appear on map unless we add a Marker layer or mock a polygon around the point.
// For MVP, we will mock a tiny square polygon around the point so it renders.
const lat = newPropertyLocation.lat;
const lng = newPropertyLocation.lng;
const size = 0.0001; // Tiny box
newProp.polygon = [
[lat + size, lng - size],
[lat + size, lng + size],
[lat - size, lng + size],
[lat - size, lng - size]
];
setProperties(prev => [...prev, newProp]);
setSelectedPropertyId(newId); // Select the new item
setNewPropertyLocation(null); // Clear creation mode
}
toast.success("Property data saved successfully");
} catch (error) {
logger.error("Error saving property data", error);
toast.error("Failed to save changes", { description: "Please try again." });
}
};
if (isLoading) return ;
return (
{ setSelectedPropertyId(null); setNewPropertyLocation(null); }}
data={selectedProperty}
newLocation={newPropertyLocation}
onSave={handleSave}
loadingAddr={loadingAddr}
/>
);
}
export default Maps;