feat(site-finder): clickable parcel markers open analysis from the map
Marker click opens an interactive Leaflet popup with a primary "Открыть анализ" action that navigates to /site-finder/analysis/<cad> (encodeURIComponent — matches the [cad] route's single-decode contract) and records the visit in RecentParcels. Hover tooltip enriched with status + actionable hint. Existing ParcelDrawer preview kept, reachable via the popup's secondary "Подробнее". ESLint + tsc clean.
This commit is contained in:
parent
51966b38c8
commit
3cea915e48
2 changed files with 104 additions and 12 deletions
|
|
@ -10,7 +10,10 @@ import { MapFilterBar } from "@/components/site-finder/entry/MapFilterBar";
|
||||||
import { ParcelDrawer } from "@/components/site-finder/entry/ParcelDrawer";
|
import { ParcelDrawer } from "@/components/site-finder/entry/ParcelDrawer";
|
||||||
import { ParcelLegend } from "@/components/site-finder/entry/ParcelLegend";
|
import { ParcelLegend } from "@/components/site-finder/entry/ParcelLegend";
|
||||||
import { RecentParcels } from "@/components/site-finder/entry/RecentParcels";
|
import { RecentParcels } from "@/components/site-finder/entry/RecentParcels";
|
||||||
import { useParcelsBboxQuery } from "@/lib/site-finder-api";
|
import {
|
||||||
|
addLocalRecentParcel,
|
||||||
|
useParcelsBboxQuery,
|
||||||
|
} from "@/lib/site-finder-api";
|
||||||
import type {
|
import type {
|
||||||
ParcelBboxFilters,
|
ParcelBboxFilters,
|
||||||
ParcelBboxItem,
|
ParcelBboxItem,
|
||||||
|
|
@ -85,6 +88,18 @@ export default function SiteFinderPage() {
|
||||||
setSelectedParcel(null);
|
setSelectedParcel(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleParcelOpen(parcel: ParcelBboxItem) {
|
||||||
|
// Record the visit so RecentParcels stays populated, then navigate.
|
||||||
|
addLocalRecentParcel({
|
||||||
|
cad_num: parcel.cad_num,
|
||||||
|
address: parcel.address,
|
||||||
|
area_ha: parcel.area_ha,
|
||||||
|
district: parcel.district,
|
||||||
|
visited_at: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
router.push(`/site-finder/analysis/${encodeURIComponent(parcel.cad_num)}`);
|
||||||
|
}
|
||||||
|
|
||||||
function handleCadSubmit(cad: string) {
|
function handleCadSubmit(cad: string) {
|
||||||
router.push(`/site-finder/analysis/${encodeURIComponent(cad)}`);
|
router.push(`/site-finder/analysis/${encodeURIComponent(cad)}`);
|
||||||
}
|
}
|
||||||
|
|
@ -162,6 +177,7 @@ export default function SiteFinderPage() {
|
||||||
<EntryMap
|
<EntryMap
|
||||||
filters={filters}
|
filters={filters}
|
||||||
selectedCad={selectedParcel?.cad_num ?? null}
|
selectedCad={selectedParcel?.cad_num ?? null}
|
||||||
|
onParcelOpen={handleParcelOpen}
|
||||||
onParcelSelect={handleParcelSelect}
|
onParcelSelect={handleParcelSelect}
|
||||||
onParcelDeselect={handleParcelDeselect}
|
onParcelDeselect={handleParcelDeselect}
|
||||||
onBboxChange={setBbox}
|
onBboxChange={setBbox}
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,11 @@ import {
|
||||||
TileLayer,
|
TileLayer,
|
||||||
CircleMarker,
|
CircleMarker,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
|
Popup,
|
||||||
useMapEvents,
|
useMapEvents,
|
||||||
} from "react-leaflet";
|
} from "react-leaflet";
|
||||||
import type { LeafletMouseEvent, Map as LeafletMap } from "leaflet";
|
import type { Map as LeafletMap } from "leaflet";
|
||||||
|
import { ArrowRight } from "lucide-react";
|
||||||
import "leaflet/dist/leaflet.css";
|
import "leaflet/dist/leaflet.css";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
|
|
@ -17,7 +19,7 @@ import type {
|
||||||
ParcelBboxFilters,
|
ParcelBboxFilters,
|
||||||
} from "@/lib/site-finder-api";
|
} from "@/lib/site-finder-api";
|
||||||
import { useParcelsBboxQuery } from "@/lib/site-finder-api";
|
import { useParcelsBboxQuery } from "@/lib/site-finder-api";
|
||||||
import { STATUS_COLORS } from "./ParcelLegend";
|
import { STATUS_COLORS, STATUS_LABELS } from "./ParcelLegend";
|
||||||
|
|
||||||
// ── Bbox change listener ───────────────────────────────────────────────────────
|
// ── Bbox change listener ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
@ -67,14 +69,23 @@ function BboxListener({ onBboxChange }: BboxListenerProps) {
|
||||||
interface ParcelMarkersProps {
|
interface ParcelMarkersProps {
|
||||||
parcels: ParcelBboxItem[];
|
parcels: ParcelBboxItem[];
|
||||||
selectedCad: string | null;
|
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;
|
onSelect: (parcel: ParcelBboxItem) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ParcelMarkers({ parcels, selectedCad, onSelect }: ParcelMarkersProps) {
|
function ParcelMarkers({
|
||||||
|
parcels,
|
||||||
|
selectedCad,
|
||||||
|
onOpen,
|
||||||
|
onSelect,
|
||||||
|
}: ParcelMarkersProps) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{parcels.map((parcel) => {
|
{parcels.map((parcel) => {
|
||||||
const color = STATUS_COLORS[parcel.status] ?? "#73767E";
|
const color = STATUS_COLORS[parcel.status] ?? "#73767E";
|
||||||
|
const statusLabel = STATUS_LABELS[parcel.status] ?? parcel.status;
|
||||||
const isSelected = parcel.cad_num === selectedCad;
|
const isSelected = parcel.cad_num === selectedCad;
|
||||||
return (
|
return (
|
||||||
<CircleMarker
|
<CircleMarker
|
||||||
|
|
@ -87,21 +98,81 @@ function ParcelMarkers({ parcels, selectedCad, onSelect }: ParcelMarkersProps) {
|
||||||
fillOpacity: 0.85,
|
fillOpacity: 0.85,
|
||||||
weight: isSelected ? 2.5 : 1.5,
|
weight: isSelected ? 2.5 : 1.5,
|
||||||
}}
|
}}
|
||||||
eventHandlers={{
|
// Leaflet renders interactive vector markers with a pointer cursor
|
||||||
click: (e: LeafletMouseEvent) => {
|
// and keeps them keyboard-focusable; click opens the Popup below.
|
||||||
e.originalEvent.stopPropagation();
|
|
||||||
onSelect(parcel);
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
|
{/* Hover hint — quick identity without committing to the analyze POST */}
|
||||||
<Tooltip direction="top" offset={[0, -8]} opacity={0.95}>
|
<Tooltip direction="top" offset={[0, -8]} opacity={0.95}>
|
||||||
<div style={{ fontSize: 12 }}>
|
<div style={{ fontSize: 12 }}>
|
||||||
<div style={{ fontWeight: 600 }}>{parcel.cad_num}</div>
|
<div style={{ fontWeight: 600 }}>{parcel.cad_num}</div>
|
||||||
<div style={{ color: "#5B6066" }}>
|
<div style={{ color: "var(--fg-secondary)" }}>
|
||||||
{parcel.area_ha.toFixed(2)} га · {parcel.district}
|
{parcel.area_ha.toFixed(2)} га · {statusLabel}
|
||||||
|
</div>
|
||||||
|
<div style={{ color: "var(--fg-tertiary)", marginTop: 2 }}>
|
||||||
|
Нажмите, чтобы открыть анализ
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
|
{/* Click target — interactive popup with the primary navigate action */}
|
||||||
|
<Popup>
|
||||||
|
<div style={{ fontSize: 12, minWidth: 168 }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontWeight: 600,
|
||||||
|
color: "var(--fg-primary)",
|
||||||
|
fontVariantNumeric: "tabular-nums",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{parcel.cad_num}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{ color: "var(--fg-secondary)", marginBottom: 8 }}
|
||||||
|
>
|
||||||
|
{parcel.area_ha.toFixed(2)} га · {statusLabel}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => onOpen(parcel)}
|
||||||
|
aria-label={`Открыть анализ участка ${parcel.cad_num}`}
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
gap: 6,
|
||||||
|
width: "100%",
|
||||||
|
background: "var(--accent)",
|
||||||
|
color: "#fff",
|
||||||
|
border: "none",
|
||||||
|
borderRadius: 6,
|
||||||
|
padding: "6px 12px",
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: 500,
|
||||||
|
cursor: "pointer",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Открыть анализ
|
||||||
|
<ArrowRight size={14} strokeWidth={1.5} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => onSelect(parcel)}
|
||||||
|
style={{
|
||||||
|
display: "block",
|
||||||
|
width: "100%",
|
||||||
|
background: "none",
|
||||||
|
border: "none",
|
||||||
|
color: "var(--fg-secondary)",
|
||||||
|
padding: "6px 12px 0",
|
||||||
|
fontSize: 12,
|
||||||
|
cursor: "pointer",
|
||||||
|
textAlign: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Подробнее в карточке
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Popup>
|
||||||
</CircleMarker>
|
</CircleMarker>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
@ -118,6 +189,9 @@ const DEFAULT_ZOOM = 12;
|
||||||
interface EntryMapProps {
|
interface EntryMapProps {
|
||||||
filters: ParcelBboxFilters;
|
filters: ParcelBboxFilters;
|
||||||
selectedCad: string | null;
|
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;
|
onParcelSelect: (parcel: ParcelBboxItem) => void;
|
||||||
onParcelDeselect: () => void;
|
onParcelDeselect: () => void;
|
||||||
/** Called whenever the map viewport changes — allows parent to read bbox for filter counters */
|
/** Called whenever the map viewport changes — allows parent to read bbox for filter counters */
|
||||||
|
|
@ -127,6 +201,7 @@ interface EntryMapProps {
|
||||||
export function EntryMap({
|
export function EntryMap({
|
||||||
filters,
|
filters,
|
||||||
selectedCad,
|
selectedCad,
|
||||||
|
onParcelOpen,
|
||||||
onParcelSelect,
|
onParcelSelect,
|
||||||
onParcelDeselect,
|
onParcelDeselect,
|
||||||
onBboxChange,
|
onBboxChange,
|
||||||
|
|
@ -163,6 +238,7 @@ export function EntryMap({
|
||||||
<ParcelMarkers
|
<ParcelMarkers
|
||||||
parcels={parcels}
|
parcels={parcels}
|
||||||
selectedCad={selectedCad}
|
selectedCad={selectedCad}
|
||||||
|
onOpen={onParcelOpen}
|
||||||
onSelect={onParcelSelect}
|
onSelect={onParcelSelect}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue