fix(site-finder): попап конкурента — NUMERIC-поля приходят строкой (цена/площадь не рендерились) #2114
2 changed files with 23 additions and 13 deletions
|
|
@ -75,9 +75,19 @@ function formatDistance(m: number | null | undefined): string | null {
|
||||||
return `${Math.round(m).toLocaleString("ru-RU")} м`;
|
return `${Math.round(m).toLocaleString("ru-RU")} м`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatPrice(rub: number | null | undefined): string | null {
|
// Бэкенд отдаёт NUMERIC-поля (avg_price_per_m2_rub, avg_area_pd) как СТРОКИ
|
||||||
if (rub === null || rub === undefined || !Number.isFinite(rub)) return null;
|
// («160345», «47.5») — Pydantic v2 сериализует Decimal в строку. Приводим к числу,
|
||||||
return `${Math.round(rub).toLocaleString("ru-RU")} ₽/м²`;
|
// иначе 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 {
|
function formatReadyDt(iso: string | null | undefined): string | null {
|
||||||
|
|
@ -406,12 +416,11 @@ export function MarketLayers({
|
||||||
{priceStr && (
|
{priceStr && (
|
||||||
<div style={{ color: "#374151" }}>{priceStr}</div>
|
<div style={{ color: "#374151" }}>{priceStr}</div>
|
||||||
)}
|
)}
|
||||||
{typeof c.avg_area_pd === "number" &&
|
{toFiniteNumber(c.avg_area_pd) !== null && (
|
||||||
Number.isFinite(c.avg_area_pd) && (
|
<div style={{ color: "#374151" }}>
|
||||||
<div style={{ color: "#374151" }}>
|
Ср. площадь кв.: {toFiniteNumber(c.avg_area_pd)} м²
|
||||||
Ср. площадь кв.: {c.avg_area_pd} м²
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
|
||||||
{showSales && (
|
{showSales && (
|
||||||
<div style={{ color: "#374151" }}>
|
<div style={{ color: "#374151" }}>
|
||||||
Продано: {c.units_sold ?? 0}
|
Продано: {c.units_sold ?? 0}
|
||||||
|
|
|
||||||
|
|
@ -50,13 +50,14 @@ export interface ParcelAnalysisCompetitor {
|
||||||
lon?: number | null;
|
lon?: number | null;
|
||||||
// Рыночные метрики из objective_lots (LEFT JOIN → null без маппинга).
|
// Рыночные метрики из objective_lots (LEFT JOIN → null без маппинга).
|
||||||
// Используются в popup-слое (цена/распроданность как velocity-прокси).
|
// Используются в 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_sold?: number | null;
|
||||||
units_available?: number | null;
|
units_available?: number | null;
|
||||||
// #2111 — средняя площадь квартиры (м²) из objective_lots. Backend кладёт через
|
// #2111 — средняя площадь квартиры (м²) из objective_lots. Backend кладёт через
|
||||||
// loose competitors[] dict-passthrough. Optional + nullable: тонкий стаб/старый
|
// loose competitors[] dict-passthrough (Decimal → строка). Optional + nullable.
|
||||||
// кэш могут не содержать; popup рендерит только при наличии.
|
avg_area_pd?: number | string | null;
|
||||||
avg_area_pd?: number | null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ParcelAnalysisDistrict {
|
export interface ParcelAnalysisDistrict {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue