From 7ab5a735fa14828e1c53d72c3f679f5f86953ba3 Mon Sep 17 00:00:00 2001 From: Light1YT Date: Mon, 29 Jun 2026 13:26:30 +0500 Subject: [PATCH] =?UTF-8?q?fix(site-finder):=20report=20microcopy=20batch?= =?UTF-8?q?=20A=20(#1953)=20=E2=80=94=20gate=20titles,=20breadcrumb=20area?= =?UTF-8?q?,=20=C2=A74.2/4.3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Из live-walkthrough Anton'а по 66:41:0702017:131: - GateVerdictBanner: сырые gate-коды → человеческие заголовки (PZZ_*/ZOUIT_*), сам код оставлен мелким приглушённым моно-тегом; prefix-tolerant для ZOUIT_SUB* - AnalysisPageContent: площадь в хлебной крошке из ЕГРН area_m2 (= KPI «Площадь»/ ЕГРН, 11.68 га) вместо geometry_suitability.area_ha (было 6.61) - BestLayoutsBlock: разделитель + строчная «покрытие» в §4.2 «Качество данных» (слипалось «НизкоеПокрытие») - VelocityBlock: «нет данных velocity» → «нет данных о темпе продаж»; empty-state для таблицы «По комнатности» вместо голых заголовков Edit-only, API-контракт не менялся (codegen не нужен). --- .../analysis/[cad]/AnalysisPageContent.tsx | 13 +- .../site-finder/BestLayoutsBlock.tsx | 11 +- .../site-finder/GateVerdictBanner.tsx | 80 +++++++- .../components/site-finder/VelocityBlock.tsx | 186 ++++++++++-------- 4 files changed, 190 insertions(+), 100 deletions(-) diff --git a/frontend/src/app/site-finder/analysis/[cad]/AnalysisPageContent.tsx b/frontend/src/app/site-finder/analysis/[cad]/AnalysisPageContent.tsx index 4f6ac20c..ebfc1943 100644 --- a/frontend/src/app/site-finder/analysis/[cad]/AnalysisPageContent.tsx +++ b/frontend/src/app/site-finder/analysis/[cad]/AnalysisPageContent.tsx @@ -14,7 +14,7 @@ import { Section4Estimate } from "@/components/site-finder/analysis/Section4Esti import { Section5Atmosphere } from "@/components/site-finder/analysis/Section5Atmosphere"; import { Section6Forecast } from "@/components/site-finder/analysis/Section6Forecast"; import { Section7Concept } from "@/components/site-finder/analysis/Section7Concept"; -import { useParcelAnalyzeQuery } from "@/lib/site-finder-api"; +import { adaptEgrn, useParcelAnalyzeQuery } from "@/lib/site-finder-api"; import type { ParcelAnalysis } from "@/types/site-finder"; // ── Props ───────────────────────────────────────────────────────────────────── @@ -114,8 +114,15 @@ export function AnalysisPageContent({ cad }: Props) { const analysis = data as unknown as ParcelAnalysis; const districtName = analysis.district?.district_name ?? "—"; - const areaHa = analysis.geometry_suitability?.area_ha; - const areaStr = areaHa != null ? `${areaHa.toFixed(2)} га` : null; + // Площадь крошки — из ТОГО ЖЕ источника, что KPI «Площадь» и ЕГРН-таблица в + // Section1: ЕГРН-area (egrn_block.area_m2 через adaptEgrn), а НЕ geometry-area + // (geometry_suitability.area_ha считается по геометрии кадастрового квартала и + // расходится с зарегистрированной площадью участка — давало 6.61 vs 11.68 га). + const egrnAreaM2 = adaptEgrn(analysis.egrn, cad)?.area_m2; + const areaStr = + egrnAreaM2 != null && Number.isFinite(egrnAreaM2) && egrnAreaM2 > 0 + ? `${(egrnAreaM2 / 10000).toFixed(2)} га` + : null; return (
diff --git a/frontend/src/components/site-finder/BestLayoutsBlock.tsx b/frontend/src/components/site-finder/BestLayoutsBlock.tsx index dc9909ee..5eae1b61 100644 --- a/frontend/src/components/site-finder/BestLayoutsBlock.tsx +++ b/frontend/src/components/site-finder/BestLayoutsBlock.tsx @@ -47,8 +47,8 @@ const ROOM_BUCKET_LABELS: Record = { function DataQualityCard({ dq }: { dq: BestLayoutsResponse["data_quality"] }) { const conf = CONFIDENCE_STYLES[dq.confidence]; return ( -
- +
+ Качество данных: {conf.label} + {/* Явный разделитель + неразрывное «покрытие …» — иначе при flex-wrap бейдж + качества и текст покрытия слипались («Низкое​Покрытие 18%», #1953). */} + + · + - Покрытие {dq.velocity_coverage_pct.toFixed(0)}% ( + покрытие {dq.velocity_coverage_pct.toFixed(0)}% ( {dq.objects_with_velocity_data} из {dq.objects_total_in_radius} ЖК)
diff --git a/frontend/src/components/site-finder/GateVerdictBanner.tsx b/frontend/src/components/site-finder/GateVerdictBanner.tsx index 21d733ed..862e7967 100644 --- a/frontend/src/components/site-finder/GateVerdictBanner.tsx +++ b/frontend/src/components/site-finder/GateVerdictBanner.tsx @@ -31,7 +31,12 @@ const COLOR_BY_LABEL: Record = { Icon: CheckCircle2, iconBg: "#10b981", }, - Нельзя: { bg: "#fef2f2", border: "#f87171", Icon: XCircle, iconBg: "#ef4444" }, + Нельзя: { + bg: "#fef2f2", + border: "#f87171", + Icon: XCircle, + iconBg: "#ef4444", + }, "С ограничениями": { bg: "#fffbeb", border: "#fbbf24", @@ -52,6 +57,44 @@ const SOURCE_LABEL: Record = { no_data: "данные NSPD не подгружены", }; +// Человеческий ЗАГОЛОВОК для сырых gate-кодов из backend (gate_verdict.py). +// Бэкенд отдаёт {code, detail}; detail уже человекочитаемый — мы поверх него +// показываем короткий жирный заголовок по коду, а сам код оставляем мелким +// тех-тегом (для отладки). Семантика 1:1 с compute_gate_verdict. +const GATE_CODE_TITLE: Record = { + PZZ_NOT_RESIDENTIAL: "Зона не под жильё", + PZZ_UNKNOWN: "Зона ПЗЗ не определена", + ZOUIT_CAD_SZZ: "Санитарно-защитная зона (СЗЗ)", + ZOUIT_CAD_OTHER: "Охранная зона (ЗОУИТ)", + ZOUIT_CAD_BLOCKER: "Охранная зона инж.сетей", + ZOUIT_NETWORK_OBREMENENIE: "Сетевое обременение (инж.сети)", + ZOUIT_ENGINEERING_PARTIAL: "Охранная зона инж.сетей (частично)", + ZOUIT_OVERLAP_SUB17: "Охранная зона инж.сетей", + NO_NSPD_DUMP: "Данные НСПД не подгружены", + NO_ENGINEERING_NEARBY: "Инж.сети рядом не найдены", +}; + +// Динамические ZOUIT-коды (ZOUIT_SUB17, ZOUIT_OVERLAP_SUB17, ZOUIT_OVERLAP_SUBxx) +// несут суффикс subcategory — резолвим по префиксу к одному заголовку. +const GATE_CODE_PREFIX_TITLE: Array<[string, string]> = [ + ["ZOUIT_OVERLAP_SUB", "Охранная зона инж.сетей"], + ["ZOUIT_SUB", "Зона с особыми условиями (ЗОУИТ)"], +]; + +/** + * Человеческий заголовок для gate-кода. Сначала точное совпадение, затем + * префиксная резолюция (ZOUIT_SUB.. / ZOUIT_OVERLAP_SUB..), и graceful fallback — + * сам код как заголовок (как было до #1953), чтобы новый backend-код не «терялся». + */ +function gateCodeTitle(code: string): string { + const exact = GATE_CODE_TITLE[code]; + if (exact) return exact; + for (const [prefix, title] of GATE_CODE_PREFIX_TITLE) { + if (code.startsWith(prefix)) return title; + } + return code; +} + function blockerCountLabel(n: number): string { if (n === 1) return "1 блокер"; if (n >= 2 && n <= 4) return `${n} блокера`; @@ -64,6 +107,33 @@ function warningCountLabel(n: number): string { return `${n} предупреждений`; } +/** + * Один пункт блокера/предупреждения: жирный человеческий заголовок (по коду) + + * detail из backend (полное предложение) + сам код мелким приглушённым моно-тегом + * (для отладки, не в глаза). Цвет текста наследуется от родительского
    . + */ +function GateItem({ code, detail }: { code: string; detail: string }) { + const title = gateCodeTitle(code); + return ( +
  • +
    {title}
    + {detail &&
    {detail}
    } + + {code} + +
  • + ); +} + export function GateVerdictBanner({ verdict }: GateVerdictBannerProps) { const [expanded, setExpanded] = useState(false); @@ -208,9 +278,7 @@ export function GateVerdictBanner({ verdict }: GateVerdictBannerProps) { }} > {verdict.blockers.map((b) => ( -
  • - {b.code}: {b.detail} -
  • + ))}
@@ -243,9 +311,7 @@ export function GateVerdictBanner({ verdict }: GateVerdictBannerProps) { }} > {verdict.warnings.map((w) => ( -
  • - {w.code}: {w.detail} -
  • + ))} diff --git a/frontend/src/components/site-finder/VelocityBlock.tsx b/frontend/src/components/site-finder/VelocityBlock.tsx index 149bd55b..09b4c323 100644 --- a/frontend/src/components/site-finder/VelocityBlock.tsx +++ b/frontend/src/components/site-finder/VelocityBlock.tsx @@ -139,7 +139,7 @@ export function VelocityBlock({ velocity }: VelocityBlockProps) {
    {!dataAvailable && ( - нет данных velocity + нет данных о темпе продаж )} {isObjectiveSource && ( @@ -328,95 +328,107 @@ export function VelocityBlock({ velocity }: VelocityBlockProps) { {velocity.by_room_bucket && (
    По комнатности - - - - + + + + + + + {BUCKET_ORDER.map((b) => { + const stat = velocity.by_room_bucket?.[b]; + if (!stat) return null; + return ( + + + + + + + ); + })} + +
    !velocity.by_room_bucket?.[b]) ? ( +
    + +
    + ) : ( + + + - Комната - - - - - - - - {BUCKET_ORDER.map((b) => { - const stat = velocity.by_room_bucket?.[b]; - if (!stat) return null; - return ( - - - - - - - ); - })} - -
    - Сделок - - м² - - ЖК -
    {BUCKET_LABEL[b]} - {stat.units.toLocaleString("ru")} - - {Math.round(stat.sqm).toLocaleString("ru")} - - {stat.complexes_count} -
    +
    + Комната + + Сделок + + м² + + ЖК +
    {BUCKET_LABEL[b]} + {stat.units.toLocaleString("ru")} + + {Math.round(stat.sqm).toLocaleString("ru")} + + {stat.complexes_count} +
    + )}
    )} -- 2.45.3