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 {