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 { 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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue