diff --git a/frontend/src/components/site-finder/MarketLayers.tsx b/frontend/src/components/site-finder/MarketLayers.tsx index 3986019e..8150ba01 100644 --- a/frontend/src/components/site-finder/MarketLayers.tsx +++ b/frontend/src/components/site-finder/MarketLayers.tsx @@ -75,9 +75,19 @@ function formatDistance(m: number | null | undefined): string | null { return `${Math.round(m).toLocaleString("ru-RU")} м`; } -function formatPrice(rub: number | null | undefined): string | null { - if (rub === null || rub === undefined || !Number.isFinite(rub)) return null; - return `${Math.round(rub).toLocaleString("ru-RU")} ₽/м²`; +// Бэкенд отдаёт NUMERIC-поля (avg_price_per_m2_rub, avg_area_pd) как СТРОКИ +// («160345», «47.5») — Pydantic v2 сериализует Decimal в строку. Приводим к числу, +// иначе typeof/Number.isFinite-гварды молча роняют поле (попап не показывал цену). +function toFiniteNumber(v: number | string | null | undefined): number | null { + if (v === null || v === undefined) return null; + const n = typeof v === "string" ? Number(v) : v; + return Number.isFinite(n) ? n : null; +} + +function formatPrice(rub: number | string | null | undefined): string | null { + const n = toFiniteNumber(rub); + if (n === null) return null; + return `${Math.round(n).toLocaleString("ru-RU")} ₽/м²`; } function formatReadyDt(iso: string | null | undefined): string | null { @@ -406,12 +416,11 @@ export function MarketLayers({ {priceStr && (
{priceStr}
)} - {typeof c.avg_area_pd === "number" && - Number.isFinite(c.avg_area_pd) && ( -
- Ср. площадь кв.: {c.avg_area_pd} м² -
- )} + {toFiniteNumber(c.avg_area_pd) !== null && ( +
+ Ср. площадь кв.: {toFiniteNumber(c.avg_area_pd)} м² +
+ )} {showSales && (
Продано: {c.units_sold ?? 0} diff --git a/frontend/src/types/site-finder.ts b/frontend/src/types/site-finder.ts index b9562e2d..1ba483c3 100644 --- a/frontend/src/types/site-finder.ts +++ b/frontend/src/types/site-finder.ts @@ -50,13 +50,14 @@ export interface ParcelAnalysisCompetitor { lon?: number | null; // Рыночные метрики из objective_lots (LEFT JOIN → null без маппинга). // Используются в popup-слое (цена/распроданность как velocity-прокси). - avg_price_per_m2_rub?: number | null; + // NUMERIC-поля приходят СТРОКОЙ (Pydantic v2 сериализует Decimal в строку), + // поэтому number | string — приводить к числу через toFiniteNumber перед форматом. + avg_price_per_m2_rub?: number | string | null; units_sold?: number | null; units_available?: number | null; // #2111 — средняя площадь квартиры (м²) из objective_lots. Backend кладёт через - // loose competitors[] dict-passthrough. Optional + nullable: тонкий стаб/старый - // кэш могут не содержать; popup рендерит только при наличии. - avg_area_pd?: number | null; + // loose competitors[] dict-passthrough (Decimal → строка). Optional + nullable. + avg_area_pd?: number | string | null; } export interface ParcelAnalysisDistrict {