From ce0aaa4fb115488dcba1c1fac576b154bf725ec5 Mon Sep 17 00:00:00 2001 From: bot-backend Date: Fri, 19 Jun 2026 14:16:08 +0300 Subject: [PATCH] feat(tradein): send picked autocomplete coords to /estimate (Variant A) --- .../src/components/trade-in/AddressInput.tsx | 10 +++++++++- .../src/components/trade-in/EstimateForm.tsx | 18 +++++++++++++++--- .../src/components/trade-in/MapPicker.tsx | 8 ++++++-- tradein-mvp/frontend/src/types/trade-in.ts | 4 ++++ 4 files changed, 34 insertions(+), 6 deletions(-) diff --git a/tradein-mvp/frontend/src/components/trade-in/AddressInput.tsx b/tradein-mvp/frontend/src/components/trade-in/AddressInput.tsx index d3993002..e9b9027e 100644 --- a/tradein-mvp/frontend/src/components/trade-in/AddressInput.tsx +++ b/tradein-mvp/frontend/src/components/trade-in/AddressInput.tsx @@ -42,6 +42,7 @@ const PRESET_ADDRESSES: { label: string; full: string; kind: string }[] = [ interface Props { value: string; onChange: (val: string) => void; + onPickCoords?: (coords: { lat: number; lon: number } | null) => void; placeholder?: string; inputId?: string; } @@ -83,7 +84,7 @@ function presetsToItems(): SuggestItem[] { })); } -export function AddressInput({ value, onChange, placeholder, inputId = "addr" }: Props) { +export function AddressInput({ value, onChange, onPickCoords, placeholder, inputId = "addr" }: Props) { const [open, setOpen] = useState(false); const [items, setItems] = useState([]); const [loading, setLoading] = useState(false); @@ -143,6 +144,12 @@ export function AddressInput({ value, onChange, placeholder, inputId = "addr" }: function pick(item: SuggestItem) { lastPickedRef.current = item.full_address; onChange(item.full_address); + const hasCoords = + Number.isFinite(item.lat) && + Number.isFinite(item.lon) && + item.lat !== 0 && + item.lon !== 0; + onPickCoords?.(hasCoords ? { lat: item.lat, lon: item.lon } : null); setOpen(false); setShowPresets(false); setItems([]); @@ -189,6 +196,7 @@ export function AddressInput({ value, onChange, placeholder, inputId = "addr" }: onChange={(e) => { onChange(e.target.value); lastPickedRef.current = ""; // юзер начал редактировать — снимаем guard + onPickCoords?.(null); // ручное редактирование сбрасывает ранее выбранные coords setShowPresets(false); }} onFocus={() => { diff --git a/tradein-mvp/frontend/src/components/trade-in/EstimateForm.tsx b/tradein-mvp/frontend/src/components/trade-in/EstimateForm.tsx index d84ae807..acd46fdf 100644 --- a/tradein-mvp/frontend/src/components/trade-in/EstimateForm.tsx +++ b/tradein-mvp/frontend/src/components/trade-in/EstimateForm.tsx @@ -73,7 +73,10 @@ function validate(f: FormState): string | null { return null; } -function toPayload(f: FormState): TradeInEstimateInput { +function toPayload( + f: FormState, + coords: { lat: number; lon: number } | null, +): TradeInEstimateInput { const out: TradeInEstimateInput = { address: f.address.trim(), area_m2: Number(f.area_m2), @@ -82,6 +85,10 @@ function toPayload(f: FormState): TradeInEstimateInput { total_floors: f.total_floors !== "" ? Number(f.total_floors) : null, has_balcony: f.has_balcony, }; + if (coords) { + out.lat = coords.lat; + out.lon = coords.lon; + } if (f.year_built !== "") out.year_built = Number(f.year_built); const ht = asHouseType(f.house_type); if (ht) out.house_type = ht; @@ -103,6 +110,7 @@ export function EstimateForm({ limit = null, }: Props) { const [form, setForm] = useState(INITIAL); + const [coords, setCoords] = useState<{ lat: number; lon: number } | null>(null); const [touched, setTouched] = useState(false); const [mapOpen, setMapOpen] = useState(false); // CRM-блок свёрнут по умолчанию — экономит ~190px высоты, чтобы форма влезала @@ -124,7 +132,7 @@ export function EstimateForm({ e.preventDefault(); setTouched(true); if (canSubmit) { - onSubmit(toPayload(form)); + onSubmit(toPayload(form, coords)); } } @@ -154,6 +162,7 @@ export function EstimateForm({ setForm((s) => ({ ...s, address: val }))} + onPickCoords={setCoords} placeholder="ул. Малышева, 30 · Куйбышева, 48…" /> @@ -510,7 +519,10 @@ export function EstimateForm({ {mapOpen && ( setForm((s) => ({ ...s, address: addr }))} + onPick={(addr, mapCoords) => { + setForm((s) => ({ ...s, address: addr })); + setCoords(mapCoords ?? null); + }} onClose={() => setMapOpen(false)} /> )} diff --git a/tradein-mvp/frontend/src/components/trade-in/MapPicker.tsx b/tradein-mvp/frontend/src/components/trade-in/MapPicker.tsx index 316f9461..ba2072ca 100644 --- a/tradein-mvp/frontend/src/components/trade-in/MapPicker.tsx +++ b/tradein-mvp/frontend/src/components/trade-in/MapPicker.tsx @@ -86,7 +86,7 @@ function loadLeaflet(): Promise { } interface Props { - onPick: (address: string) => void; + onPick: (address: string, coords?: { lat: number; lon: number }) => void; onClose: () => void; } @@ -99,6 +99,8 @@ export function MapPicker({ onPick, onClose }: Props) { // тонкий hint «Точка дома по Яндексу», чтобы user понимал почему marker // переехал на пару метров от его клика. const [snapped, setSnapped] = useState(false); + // Координаты последнего выбора (snap-точка здания или raw клик). + const pickedCoordsRef = useRef<{ lat: number; lon: number } | null>(null); // Portal-mount guard (SSR-safe): document доступен только после монтирования. // Рендерим оверлей в document.body, чтобы position:fixed/z-index:1000 не были // заперты в stacking-context формы (#771: полоски SourcesProgress наезжали). @@ -123,6 +125,7 @@ export function MapPicker({ onPick, onClose }: Props) { map.on("click", (e: any) => { const lat = e.latlng.lat as number; const lon = e.latlng.lng as number; + pickedCoordsRef.current = { lat, lon }; // сохраняем raw click coords if (marker) marker.setLatLng([lat, lon]); else marker = L.circleMarker([lat, lon], { @@ -162,6 +165,7 @@ export function MapPicker({ onPick, onClose }: Props) { animate: true, duration: 0.3, }); + pickedCoordsRef.current = { lat: d.snapped_lat, lon: d.snapped_lon }; setSnapped(true); } } @@ -312,7 +316,7 @@ export function MapPicker({ onPick, onClose }: Props) { type="button" onClick={() => { if (address) { - onPick(address); + onPick(address, pickedCoordsRef.current ?? undefined); onClose(); } }} diff --git a/tradein-mvp/frontend/src/types/trade-in.ts b/tradein-mvp/frontend/src/types/trade-in.ts index 3b9c9cd4..b239c780 100644 --- a/tradein-mvp/frontend/src/types/trade-in.ts +++ b/tradein-mvp/frontend/src/types/trade-in.ts @@ -60,6 +60,10 @@ export interface TradeInEstimateInput { has_mortgage?: boolean; client_name?: string; client_phone?: string; + // Координаты из автокомплита — позволяют бэкенду пропустить геокодинг + // для адресов, которые DaData/Yandex не резолвят по строке. + lat?: number | null; + lon?: number | null; } export interface AnalogLot {