"use client"; import { useCallback, useEffect, useRef, useState } from "react"; import { MapContainer, TileLayer, CircleMarker, Tooltip, Popup, useMapEvents, } from "react-leaflet"; import type { Map as LeafletMap } from "leaflet"; import { ArrowRight } from "lucide-react"; import "leaflet/dist/leaflet.css"; import type { ParcelBboxItem, BboxCoords, ParcelBboxFilters, } from "@/lib/site-finder-api"; import { useParcelsBboxQuery } from "@/lib/site-finder-api"; import { STATUS_COLORS, STATUS_LABELS } from "./ParcelLegend"; // ── Bbox change listener ─────────────────────────────────────────────────────── interface BboxListenerProps { onBboxChange: (bbox: BboxCoords) => void; } function BboxListener({ onBboxChange }: BboxListenerProps) { const map = useMapEvents({ moveend() { const bounds = map.getBounds(); onBboxChange({ minLat: bounds.getSouth(), minLon: bounds.getWest(), maxLat: bounds.getNorth(), maxLon: bounds.getEast(), }); }, zoomend() { const bounds = map.getBounds(); onBboxChange({ minLat: bounds.getSouth(), minLon: bounds.getWest(), maxLat: bounds.getNorth(), maxLon: bounds.getEast(), }); }, }); // Emit initial bbox on mount useEffect(() => { const bounds = map.getBounds(); onBboxChange({ minLat: bounds.getSouth(), minLon: bounds.getWest(), maxLat: bounds.getNorth(), maxLon: bounds.getEast(), }); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); return null; } // ── Parcel markers ───────────────────────────────────────────────────────────── interface ParcelMarkersProps { parcels: ParcelBboxItem[]; selectedCad: string | null; /** Open the full analysis page for a parcel (primary action). */ onOpen: (parcel: ParcelBboxItem) => void; /** Open the side drawer with a parcel preview (secondary action). */ onSelect: (parcel: ParcelBboxItem) => void; } function ParcelMarkers({ parcels, selectedCad, onOpen, onSelect, }: ParcelMarkersProps) { return ( <> {parcels.map((parcel) => { const color = STATUS_COLORS[parcel.status] ?? "#73767E"; const statusLabel = STATUS_LABELS[parcel.status] ?? parcel.status; const isSelected = parcel.cad_num === selectedCad; return ( {/* Hover hint — quick identity without committing to the analyze POST */}
{parcel.cad_num}
{parcel.area_ha.toFixed(2)} га · {statusLabel}
Нажмите, чтобы открыть анализ
{/* Click target — interactive popup with the primary navigate action */}
{parcel.cad_num}
{parcel.area_ha.toFixed(2)} га · {statusLabel}
); })} ); } // ── EntryMap ─────────────────────────────────────────────────────────────────── // Yekaterinburg center const EKB_CENTER: [number, number] = [56.8389, 60.6057]; const DEFAULT_ZOOM = 12; interface EntryMapProps { filters: ParcelBboxFilters; selectedCad: string | null; /** Primary action: navigate to the analysis page for a parcel. */ onParcelOpen: (parcel: ParcelBboxItem) => void; /** Secondary action: open the preview drawer for a parcel. */ onParcelSelect: (parcel: ParcelBboxItem) => void; onParcelDeselect: () => void; /** Called whenever the map viewport changes — allows parent to read bbox for filter counters */ onBboxChange?: (bbox: BboxCoords) => void; } export function EntryMap({ filters, selectedCad, onParcelOpen, onParcelSelect, onParcelDeselect, onBboxChange, }: EntryMapProps) { const [bbox, setBbox] = useState(null); const mapRef = useRef(null); const { data: parcels, isFetching } = useParcelsBboxQuery(bbox, filters); const handleBboxChange = useCallback( (newBbox: BboxCoords) => { setBbox(newBbox); onBboxChange?.(newBbox); }, [onBboxChange], ); return (
{parcels && parcels.length > 0 && ( )} {/* Deselect on map click — overlay to capture clicks outside markers */} {selectedCad && ( ); }