diff --git a/frontend/src/app/site-finder/page.tsx b/frontend/src/app/site-finder/page.tsx index 460a9c00..568b146f 100644 --- a/frontend/src/app/site-finder/page.tsx +++ b/frontend/src/app/site-finder/page.tsx @@ -17,6 +17,7 @@ import { WeightProfilePanel } from "@/components/site-finder/WeightProfilePanel" import { useSiteAnalysis } from "@/hooks/useSiteAnalysis"; import { useDebouncedValue } from "@/hooks/useDebouncedValue"; import { useConnectionPoints } from "@/hooks/useConnectionPoints"; +import { useCustomPois } from "@/hooks/useCustomPois"; import { POI_DEFAULT_WEIGHTS, type PoiCategoryKey, @@ -101,6 +102,8 @@ function SiteFinderContent() { // Fetch connection points whenever a parcel is loaded const { data: connectionPoints } = useConnectionPoints(data?.cad_num); + // Fetch custom POIs for current parcel + const { data: customPois } = useCustomPois(data?.cad_num); // Weight profile state — lifted here so it survives tab switches. // userId + adminToken allow the panel to load/save named profiles. @@ -545,6 +548,8 @@ function SiteFinderContent() { data={data} isochrones={isochrones} connectionPoints={connectionPoints} + customPois={customPois} + parcelCad={data.cad_num} /> diff --git a/frontend/src/components/site-finder/CustomPoiAddModal.tsx b/frontend/src/components/site-finder/CustomPoiAddModal.tsx new file mode 100644 index 00000000..1c9c8201 --- /dev/null +++ b/frontend/src/components/site-finder/CustomPoiAddModal.tsx @@ -0,0 +1,332 @@ +"use client"; + +import { useState } from "react"; +import type { CustomPoiCreate } from "@/types/customPoi"; + +// --------------------------------------------------------------------------- +// OSM POI category options (25 common + "Другое") +// --------------------------------------------------------------------------- + +const CATEGORY_OPTIONS = [ + "school", + "kindergarten", + "pharmacy", + "hospital", + "clinic", + "shop_mall", + "shop_supermarket", + "shop_small", + "park", + "tram_stop", + "bus_stop", + "metro_stop", + "cafe", + "restaurant", + "bank", + "atm", + "post_office", + "library", + "sports_centre", + "gym", + "cinema", + "theatre", + "hotel", + "fuel", + "parking", + "Другое", +] as const; + +// --------------------------------------------------------------------------- +// Props +// --------------------------------------------------------------------------- + +interface Props { + lon: number; + lat: number; + parcelCad?: string | null; + onSubmit: (data: CustomPoiCreate) => void; + onCancel: () => void; + isLoading?: boolean; +} + +// --------------------------------------------------------------------------- +// Component +// --------------------------------------------------------------------------- + +export function CustomPoiAddModal({ + lon, + lat, + parcelCad, + onSubmit, + onCancel, + isLoading = false, +}: Props) { + const [name, setName] = useState(""); + const [category, setCategory] = useState("Другое"); + const [weight, setWeight] = useState(0); + const [notes, setNotes] = useState(""); + const [nameError, setNameError] = useState(null); + + function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + if (!name.trim()) { + setNameError("Название обязательно"); + return; + } + setNameError(null); + onSubmit({ + name: name.trim(), + category: category === "Другое" ? null : category, + weight, + lon, + lat, + parcel_cad: parcelCad ?? null, + notes: notes.trim() || null, + }); + } + + // Derived color for weight indicator + const weightColor = + weight > 0 ? "#16a34a" : weight < 0 ? "#dc2626" : "#6b7280"; + + return ( + /* Backdrop */ +
{ + if (e.target === e.currentTarget) onCancel(); + }} + > +
e.stopPropagation()} + > +

+ Добавить точку интереса +

+ +
+ {/* Coordinates (read-only) */} +
+ {lat.toFixed(5)}, {lon.toFixed(5)} + {parcelCad && ( + + · {parcelCad} + + )} +
+ + {/* Name */} +
+ + { + setName(e.target.value); + setNameError(null); + }} + placeholder="Например: Детская площадка" + style={{ + width: "100%", + padding: "8px 10px", + fontSize: 13, + border: `1px solid ${nameError ? "#f87171" : "#d1d5db"}`, + borderRadius: 7, + boxSizing: "border-box", + }} + autoFocus + /> + {nameError && ( +

+ {nameError} +

+ )} +
+ + {/* Category */} +
+ + +
+ + {/* Weight slider */} +
+ + setWeight(Number(e.target.value))} + style={{ width: "100%" }} + /> +
+ -5 (отрицательный) + 0 + +5 (положительный) +
+
+ + {/* Notes */} +
+ +