Merge pull request 'feat(tradein): фронт передаёт координаты выбранной подсказки в /estimate (Variant A, frontend)' (#1783) from feat/tradein-estimate-send-coords into main
All checks were successful
Deploy Trade-In / changes (push) Successful in 5s
Deploy Trade-In / test (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / build-backend (push) Has been skipped
Deploy Trade-In / build-frontend (push) Successful in 1m44s
Deploy Trade-In / deploy (push) Successful in 45s

Reviewed-on: #1783
This commit is contained in:
lekss361 2026-06-19 11:17:31 +00:00
commit 190accc4a7
4 changed files with 34 additions and 6 deletions

View file

@ -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<SuggestItem[]>([]);
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={() => {

View file

@ -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<FormState>(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({
<AddressInput
value={form.address}
onChange={(val) => setForm((s) => ({ ...s, address: val }))}
onPickCoords={setCoords}
placeholder="ул. Малышева, 30 · Куйбышева, 48…"
/>
</div>
@ -510,7 +519,10 @@ export function EstimateForm({
{mapOpen && (
<MapPicker
onPick={(addr) => setForm((s) => ({ ...s, address: addr }))}
onPick={(addr, mapCoords) => {
setForm((s) => ({ ...s, address: addr }));
setCoords(mapCoords ?? null);
}}
onClose={() => setMapOpen(false)}
/>
)}

View file

@ -86,7 +86,7 @@ function loadLeaflet(): Promise<any> {
}
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();
}
}}

View file

@ -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 {