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, ShieldCheck } 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 PropertyDetailDrawer from '../components/maps/PropertyDetailDrawer'; // --- ICONS --- 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-white border border-zinc-200 text-zinc-900 shadow-sm" }; return ( {status} ); }; // Map Interaction Component const MapInteraction = ({ onMapClick }) => { useMapEvents({ click(e) { onMapClick(e.latlng); }, }); return null; }; const MapLegend = () => { const [isOpen, setIsOpen] = useState(false); return ( <> {/* Mobile Toggle - Top Right */} {/* Legend Content - Above Toggle on Mobile */}

Map Legend

Hot Lead
Customer
Renovated
Neutral
Not Interested
); }; // Map Resizer Component const MapResizer = () => { const map = useMapEvents({}); useEffect(() => { const resizeObserver = new ResizeObserver(() => { map.invalidateSize(); }); const container = map.getContainer(); resizeObserver.observe(container); return () => { resizeObserver.disconnect(); }; }, [map]); return null; }; // 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 = "#ffffff"; // Bright White fillOpacity = 0.4; // Higher opacity for white visibility break; default: // Neutral (Violet) break; } return ( { L.DomEvent.stopPropagation(e); // Prevent map click onSelect(item); }, }} /> ); })} ); }; // --- 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) => { // Field Agents, Admins, and Owners can create new map entries if (!['FIELD_AGENT', 'ADMIN', 'OWNER'].includes(user?.role)) 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 (Enhanced) isRented: formData.isRented === 'Yes', currentlyRented: formData.isRented === 'Yes', // Sync both just in case currentTenantName: formData.tenantName, tenantPhone: formData.tenantPhone, tenantEmail: formData.tenantEmail, tenantOccupation: formData.tenantOccupation, tenantEmployer: formData.tenantEmployer, tenantAnnualIncome: formData.tenantIncome, livingStatus: formData.livingStatus, leaseType: formData.leaseType, leaseSignedDate: formData.leaseSigned, leaseStartDate: formData.leaseStart, leaseEndDate: formData.leaseEnd, currentMonthlyRentAmount: formData.currentRent, ownerWillingToRent: formData.ownerWillingToRent === 'Yes', expectedMonthlyRentAmount: formData.expectedRent, // Location (can be expanded) schoolDistrict: formData.schoolDistrict, neighborhoodRating: formData.neighborhoodRating } }; const newInsuranceData = { insurance_company: formData.insurance_company, insurance_company_not_listed: formData.insurance_company_not_listed, damage_location: formData.damage_location, date_of_loss: formData.date_of_loss, claim_filed: formData.claim_filed === 'Yes', claim_number: formData.claim_number, has_paperwork: formData.has_paperwork === 'Yes', adjuster_name: formData.adjuster_name, adjuster_phone: formData.adjuster_phone, adjuster_ext: formData.adjuster_ext, adjuster_type: formData.adjuster_type, adjuster_fax: formData.adjuster_fax, adjuster_email: formData.adjuster_email, met_with_adjuster: formData.met_with_adjuster === 'Yes', claim_approved: formData.claim_approved === 'Yes', }; if (selectedPropertyId) { // Update Existing setProperties(prev => prev.map(p => { if (p.id === selectedPropertyId) { return { ...p, canvassingStatus: formData.status, ownerData: { ...p.ownerData, ...newOwnerData }, propertyData: { ...p.propertyData, ...newPropertyData.details, ...newPropertyData }, // Merge Flattened and Nested insuranceData: { ...p.insuranceData, ...newInsuranceData } }; } return p; })); toast.success("Property updated successfully"); } else if (newPropertyLocation) { // Create New const newId = properties.length + 1; // Generate Polygon (Simple Box around center) as placeholder // In real app, user would draw. Here we just spawn a box. const d = 0.0001; const newPoly = [ [newPropertyLocation.lat + d, newPropertyLocation.lng - d], [newPropertyLocation.lat + d, newPropertyLocation.lng + d], [newPropertyLocation.lat - d, newPropertyLocation.lng + d], [newPropertyLocation.lat - d, newPropertyLocation.lng - d], ]; const newItem = { id: newId, center: [newPropertyLocation.lat, newPropertyLocation.lng], polygon: newPoly, canvassingStatus: formData.status, ownerData: { ownerId: `OWN-NEW-${newId}`, ...newOwnerData }, propertyData: { propertyId: `P-NEW-${newId}`, propertyAddress: newPropertyLocation.address, ...newPropertyData.details, ...newPropertyData }, insuranceData: { ...newInsuranceData } }; setProperties(prev => [...prev, newItem]); toast.success("New property added to map"); setSelectedPropertyId(newId); // Select it setNewPropertyLocation(null); // Exit create mode } } catch (err) { logger.error("Failed to save property", err); toast.error("Failed to save changes", { description: "Please check your input." }); } }; if (isLoading) return ; return (
{ setSelectedPropertyId(null); setNewPropertyLocation(null); }} data={selectedProperty} newLocation={newPropertyLocation} loadingAddr={loadingAddr} onSave={handleSave} /> igotsar.matyas | LynkedUpPro - Turns out roofing CRMs don't build themselves. Who knew?
); } export default Maps;