feat(site-finder): clickable parcel markers open analysis from the map
All checks were successful
Deploy / changes (push) Successful in 6s
Deploy / build-backend (push) Has been skipped
Deploy / build-worker (push) Has been skipped
Deploy / build-frontend (push) Successful in 2m47s
Deploy / deploy (push) Successful in 1m7s

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:
Light1YT 2026-06-04 12:50:37 +05:00
parent 51966b38c8
commit 3cea915e48
2 changed files with 104 additions and 12 deletions

View file

@ -10,7 +10,10 @@ import { MapFilterBar } from "@/components/site-finder/entry/MapFilterBar";
import { ParcelDrawer } from "@/components/site-finder/entry/ParcelDrawer";
import { ParcelLegend } from "@/components/site-finder/entry/ParcelLegend";
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 {
ParcelBboxFilters,
ParcelBboxItem,
@ -85,6 +88,18 @@ export default function SiteFinderPage() {
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) {
router.push(`/site-finder/analysis/${encodeURIComponent(cad)}`);
}
@ -162,6 +177,7 @@ export default function SiteFinderPage() {
<EntryMap
filters={filters}
selectedCad={selectedParcel?.cad_num ?? null}
onParcelOpen={handleParcelOpen}
onParcelSelect={handleParcelSelect}
onParcelDeselect={handleParcelDeselect}
onBboxChange={setBbox}

View file

@ -6,9 +6,11 @@ import {
TileLayer,
CircleMarker,
Tooltip,
Popup,
useMapEvents,
} 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 type {
@ -17,7 +19,7 @@ import type {
ParcelBboxFilters,
} 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 ───────────────────────────────────────────────────────
@ -67,14 +69,23 @@ function BboxListener({ onBboxChange }: BboxListenerProps) {
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, onSelect }: ParcelMarkersProps) {
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 (
<CircleMarker
@ -87,21 +98,81 @@ function ParcelMarkers({ parcels, selectedCad, onSelect }: ParcelMarkersProps) {
fillOpacity: 0.85,
weight: isSelected ? 2.5 : 1.5,
}}
eventHandlers={{
click: (e: LeafletMouseEvent) => {
e.originalEvent.stopPropagation();
onSelect(parcel);
},
}}
// Leaflet renders interactive vector markers with a pointer cursor
// and keeps them keyboard-focusable; click opens the Popup below.
>
{/* Hover hint — quick identity without committing to the analyze POST */}
<Tooltip direction="top" offset={[0, -8]} opacity={0.95}>
<div style={{ fontSize: 12 }}>
<div style={{ fontWeight: 600 }}>{parcel.cad_num}</div>
<div style={{ color: "#5B6066" }}>
{parcel.area_ha.toFixed(2)} га · {parcel.district}
<div style={{ color: "var(--fg-secondary)" }}>
{parcel.area_ha.toFixed(2)} га · {statusLabel}
</div>
<div style={{ color: "var(--fg-tertiary)", marginTop: 2 }}>
Нажмите, чтобы открыть анализ
</div>
</div>
</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>
);
})}
@ -118,6 +189,9 @@ 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 */
@ -127,6 +201,7 @@ interface EntryMapProps {
export function EntryMap({
filters,
selectedCad,
onParcelOpen,
onParcelSelect,
onParcelDeselect,
onBboxChange,
@ -163,6 +238,7 @@ export function EntryMap({
<ParcelMarkers
parcels={parcels}
selectedCad={selectedCad}
onOpen={onParcelOpen}
onSelect={onParcelSelect}
/>
)}