From 35db6608ee0fbf804e4094ca3b9321969d665f7b Mon Sep 17 00:00:00 2001 From: bot-backend Date: Wed, 2 Sep 2026 14:50:47 +0500 Subject: [PATCH] =?UTF-8?q?fix(tradein-v2):=20=D1=84=D1=83=D1=82=D0=B5?= =?UTF-8?q?=D1=80=2004,=20=D0=BF=D0=B0=D0=B4=D0=B5=D0=B6=20=D1=81=D0=B4?= =?UTF-8?q?=D0=B5=D0=BB=D0=BE=D0=BA=20=D0=B8=20=D1=86=D0=B2=D0=B5=D1=82=20?= =?UTF-8?q?=D0=B4=D0=B5=D0=BB=D1=8C=D1=82=D1=8B=20=D0=BF=D0=BE=20=D0=B7?= =?UTF-8?q?=D0=BD=D0=B0=D0=BA=D1=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ревью PR #3328: - HistoryView: «Показано N из M сделок» брало M из street-deals, а строки — из sales-vs-listings; после гашения пустого коридора выходило «из —». Знаменатель убран, как в 05 РЫНОК; - обе подписи склоняются через pluralRu («3 фактические сделки», «2 сделки»); - цвет дельты медианы сделок считается по знаку (mapSources.deltaColor): зелёным красились и минус, и прочерк — это второй причинный узел жалобы «−100% зелёным»; - подпись полосы совпала с карточкой 1 буквально («ПО СДЕЛКАМ РОСРЕЕСТРА»); - тест: убран тавтологичный assert про EmptyTableNote (её рисует пустой dealRows, а не guard), добавлены кейсы на футер 04 и на цвет дельты. --- .../components/trade-in/v2/HistoryView.tsx | 8 ++- .../components/trade-in/v2/SourcesView.tsx | 13 ++++- .../v2/__tests__/honestZeros.test.tsx | 51 ++++++++++++++++--- .../src/components/trade-in/v2/mappers.ts | 11 +++- .../src/components/trade-in/v2/types.ts | 14 ++++- 5 files changed, 84 insertions(+), 13 deletions(-) diff --git a/tradein-mvp/frontend/src/components/trade-in/v2/HistoryView.tsx b/tradein-mvp/frontend/src/components/trade-in/v2/HistoryView.tsx index 59fc6beb..08c66eda 100644 --- a/tradein-mvp/frontend/src/components/trade-in/v2/HistoryView.tsx +++ b/tradein-mvp/frontend/src/components/trade-in/v2/HistoryView.tsx @@ -415,7 +415,13 @@ export default function HistoryView({ data }: HistoryViewProps) { background: tokens.surfaceTint, }} > - Показано {data.dkpRows.length} из {data.dkpKpi.count} сделок + {/* #3320 — «из M» брало знаменатель из ДРУГОЙ выборки: строки ниже — + пары ДКП↔объявление (sales-vs-listings), а dkpKpi.count — коридор + street-deals. Расходились и числами («Показано 7 из 3»), и после + гашения пустого коридора («Показано 7 из —»). Тотала у той же + выборки в ответе нет — знаменатель убран, как в 05 РЫНОК. */} + Показано {data.dkpRows.length}{" "} + {pluralRu(data.dkpRows.length, ["сделка", "сделки", "сделок"])} diff --git a/tradein-mvp/frontend/src/components/trade-in/v2/SourcesView.tsx b/tradein-mvp/frontend/src/components/trade-in/v2/SourcesView.tsx index 3d151798..e930ec58 100644 --- a/tradein-mvp/frontend/src/components/trade-in/v2/SourcesView.tsx +++ b/tradein-mvp/frontend/src/components/trade-in/v2/SourcesView.tsx @@ -4,6 +4,7 @@ import type { CSSProperties, ReactNode } from "react"; import { safeUrl } from "@/lib/safeUrl"; import type { AggregatedEstimate } from "@/types/trade-in"; import { tokens } from "./tokens"; +import { pluralRu } from "./mappers"; import type { AdRowData, DealRowData, SourcesData } from "./mappers"; import { SourcesMap } from "./SourcesMap"; @@ -637,7 +638,10 @@ export default function SourcesView({ label="МЕДИАНА СДЕЛОК" value={data.marketDeals.kpi.median} unit={`млн ₽ · ${data.marketDeals.kpi.delta}`} - unitColor={tokens.success} + // #3320 — цвет по знаку дельты (её считает mapSources): зелёным + // здесь красился и минус, и прочерк. undefined → нейтральный + // дефолт плитки. + unitColor={data.marketDeals.kpi.deltaColor} /> ) : ( diff --git a/tradein-mvp/frontend/src/components/trade-in/v2/__tests__/honestZeros.test.tsx b/tradein-mvp/frontend/src/components/trade-in/v2/__tests__/honestZeros.test.tsx index 8832d700..7399241e 100644 --- a/tradein-mvp/frontend/src/components/trade-in/v2/__tests__/honestZeros.test.tsx +++ b/tradein-mvp/frontend/src/components/trade-in/v2/__tests__/honestZeros.test.tsx @@ -14,9 +14,11 @@ import { render, screen } from "@testing-library/react"; import { describe, expect, it } from "vitest"; +import HistoryView from "../HistoryView"; import SourcesView from "../SourcesView"; -import { mapResultPanel, mapSources } from "../mappers"; -import { FIXTURE_ESTIMATE } from "@/app/ui-preview/estimate/fixture"; +import { mapHistory, mapResultPanel, mapSources } from "../mappers"; +import { tokens } from "../tokens"; +import { FIXTURE_ESTIMATE, FIXTURE_SALES } from "@/app/ui-preview/estimate/fixture"; import type { AggregatedEstimate, AnalogLot, @@ -74,17 +76,31 @@ describe("05 РЫНОК — пустая выборка сделок не рис range: "—", }); + // Цвет подписи тоже гаснет: зелёным красилась даже дельта, которой нет. + expect(data.marketDeals.kpi.deltaColor).toBeUndefined(); + const { container } = render(); - // Тот же пустой блок, что и при отсутствующем ответе street-deals. - expect( - screen.getByText(/Сделок по этому адресу за 12 месяцев не найдено/), - ).toBeTruthy(); // Ни «−100% к цене объявления», ни «0,00 – 0,00» на экране быть не может. expect(container.textContent).not.toContain("−100"); expect(container.textContent).not.toMatch(/(? { + // Второй причинный узел жалобы «−100% зелёным»: цвет был захардкожен. + // Сделки по фикстуре (8,00 млн) ниже медианы объявлений (9,85 млн). + const below = mapSources(estimateWith([deal({ price_rub: 8_000_000 })]), null); + expect(below.marketDeals.kpi.delta).toContain("−"); + expect(below.marketDeals.kpi.deltaColor).toBe(tokens.danger); + + const above = mapSources( + estimateWith([deal({ price_rub: 12_000_000 })]), + null, + ); + expect(above.marketDeals.kpi.delta).toContain("+"); + expect(above.marketDeals.kpi.deltaColor).toBe(tokens.success); + }); + it("count=0, но у оценки есть свои сделки → числа из НИХ, а не прочерки", () => { // Схлопывание пустой оболочки не должно гасить реальную выборку: маппер // обязан упасть на actual_deals, чьи строки видно в таблице ниже KPI. @@ -119,8 +135,27 @@ describe("05 РЫНОК — футер таблицы сделок считае render(); - expect(screen.getByText("Показано 3 фактических сделок")).toBeTruthy(); - expect(screen.queryByText(/Показано \d+ из \d+ фактических/)).toBeNull(); + // («Показано N из M объявлений» над таблицей аналогов — законный футер + // СВОЕЙ выборки, поэтому проверяется именно подпись под сделками.) + const footer = screen.getByText(/Показано \d+ .*сделк/); + expect(footer.textContent).toBe("Показано 3 фактические сделки"); + expect(footer.textContent).not.toMatch(/\sиз\s/); + }); + + it("04 ПРОДАЖИ — футер ДКП-таблицы тоже считает свою выборку", () => { + // Строки тут — пары ДКП↔объявление, а прежний знаменатель брался из + // коридора street-deals: 2 пары против count=7, а при пустом коридоре + // (после гашения нулей) выходило «Показано 2 из —». + const data = mapHistory( + null, + { ...EMPTY_STREET_DEALS, count: 7, median_price_rub: 12_000_000 }, + { ...FIXTURE_SALES, pairs: FIXTURE_SALES.pairs.slice(0, 2) }, + ); + + render(); + + expect(screen.getByText(/Показано 2 сделки/)).toBeTruthy(); + expect(screen.queryByText(/Показано \d+ из/)).toBeNull(); }); }); diff --git a/tradein-mvp/frontend/src/components/trade-in/v2/mappers.ts b/tradein-mvp/frontend/src/components/trade-in/v2/mappers.ts index eca517da..3d1167b3 100644 --- a/tradein-mvp/frontend/src/components/trade-in/v2/mappers.ts +++ b/tradein-mvp/frontend/src/components/trade-in/v2/mappers.ts @@ -1270,7 +1270,7 @@ export function mapResultPanel( // та же, что #2629 дал карточке 1 над этой полосой («РЕКОМЕНДОВАННАЯ ЦЕНА // ПО СДЕЛКАМ РОСРЕЕСТРА»), чтобы подпись и число читались одной парой. label: dealsOnlyPrice - ? ["РЕКОМЕНДОВАННЫЙ ДИАПАЗОН", "ПО СДЕЛКАМ РОСРЕЕСТРА (ДКП)"] + ? ["РЕКОМЕНДОВАННЫЙ ДИАПАЗОН", "ПО СДЕЛКАМ РОСРЕЕСТРА"] : ["ДИАПАЗОН ЦЕН В ОБЪЯВЛЕНИЯХ", "(БЕЗ УЧЁТА РЕМОНТА)"], median: `${fmtMln(e.median_price_rub)} млн ₽`, lo: `${fmtMln(e.range_low_rub)} млн`, @@ -2240,6 +2240,15 @@ export function mapSources( // §M8 — единая формулировка дельт: всё считается «относительно цен // объявлений» (deltaPct = медиана сделок / медиана объявлений − 1). delta: deltaPct != null ? `${fmtPct(deltaPct)} к цене объявления` : "—", + // #3320 — знак → цвет, тот же признак, что у пар ДКП↔объявление в 04 + // (deltaColor у DkpRowData): продали дороже объявления — success, + // дешевле — danger, числа нет — нейтральный цвет плитки. + deltaColor: + deltaPct == null + ? undefined + : deltaPct < 0 + ? tokens.danger + : tokens.success, range: mlnRangeSp(dealLo, dealHi), }, }; diff --git a/tradein-mvp/frontend/src/components/trade-in/v2/types.ts b/tradein-mvp/frontend/src/components/trade-in/v2/types.ts index 2155e18b..61e4b8af 100644 --- a/tradein-mvp/frontend/src/components/trade-in/v2/types.ts +++ b/tradein-mvp/frontend/src/components/trade-in/v2/types.ts @@ -227,7 +227,19 @@ export interface MarketAds { } export interface MarketDeals { - kpi: { count: string; median: string; delta: string; range: string }; + kpi: { + count: string; + median: string; + delta: string; + range: string; + /** + * #3320 — цвет подписи дельты по её знаку (как deltaColor у DkpRowData): + * сделки выше объявлений — success, ниже — danger, «—» — undefined + * (плитка красит нейтральным по умолчанию). Раньше цвет был захардкожен + * зелёным и красил им же и минус, и прочерк. + */ + deltaColor?: string; + }; } // ---- OVERLAY: АНАЛИТИКА ДОМА ----------------------------------------------