fix(tradein-v2): футер 04, падеж сделок и цвет дельты по знаку
All checks were successful
CI Trade-In / changes (pull_request) Successful in 12s
CI / changes (pull_request) Successful in 13s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Successful in 1m31s
All checks were successful
CI Trade-In / changes (pull_request) Successful in 12s
CI / changes (pull_request) Successful in 13s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Successful in 1m31s
Ревью PR #3328: - HistoryView: «Показано N из M сделок» брало M из street-deals, а строки — из sales-vs-listings; после гашения пустого коридора выходило «из —». Знаменатель убран, как в 05 РЫНОК; - обе подписи склоняются через pluralRu («3 фактические сделки», «2 сделки»); - цвет дельты медианы сделок считается по знаку (mapSources.deltaColor): зелёным красились и минус, и прочерк — это второй причинный узел жалобы «−100% зелёным»; - подпись полосы совпала с карточкой 1 буквально («ПО СДЕЛКАМ РОСРЕЕСТРА»); - тест: убран тавтологичный assert про EmptyTableNote (её рисует пустой dealRows, а не guard), добавлены кейсы на футер 04 и на цвет дельты.
This commit is contained in:
parent
08cabe8d98
commit
35db6608ee
5 changed files with 84 additions and 13 deletions
|
|
@ -415,7 +415,13 @@ export default function HistoryView({ data }: HistoryViewProps) {
|
||||||
background: tokens.surfaceTint,
|
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, ["сделка", "сделки", "сделок"])}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import type { CSSProperties, ReactNode } from "react";
|
||||||
import { safeUrl } from "@/lib/safeUrl";
|
import { safeUrl } from "@/lib/safeUrl";
|
||||||
import type { AggregatedEstimate } from "@/types/trade-in";
|
import type { AggregatedEstimate } from "@/types/trade-in";
|
||||||
import { tokens } from "./tokens";
|
import { tokens } from "./tokens";
|
||||||
|
import { pluralRu } from "./mappers";
|
||||||
import type { AdRowData, DealRowData, SourcesData } from "./mappers";
|
import type { AdRowData, DealRowData, SourcesData } from "./mappers";
|
||||||
import { SourcesMap } from "./SourcesMap";
|
import { SourcesMap } from "./SourcesMap";
|
||||||
|
|
||||||
|
|
@ -637,7 +638,10 @@ export default function SourcesView({
|
||||||
label="МЕДИАНА СДЕЛОК"
|
label="МЕДИАНА СДЕЛОК"
|
||||||
value={data.marketDeals.kpi.median}
|
value={data.marketDeals.kpi.median}
|
||||||
unit={`млн ₽ · ${data.marketDeals.kpi.delta}`}
|
unit={`млн ₽ · ${data.marketDeals.kpi.delta}`}
|
||||||
unitColor={tokens.success}
|
// #3320 — цвет по знаку дельты (её считает mapSources): зелёным
|
||||||
|
// здесь красился и минус, и прочерк. undefined → нейтральный
|
||||||
|
// дефолт плитки.
|
||||||
|
unitColor={data.marketDeals.kpi.deltaColor}
|
||||||
/>
|
/>
|
||||||
<KpiCell
|
<KpiCell
|
||||||
label="ДИАПАЗОН"
|
label="ДИАПАЗОН"
|
||||||
|
|
@ -669,7 +673,12 @@ export default function SourcesView({
|
||||||
по дому/району): выходило «Показано 10 из 3». Тотала у той же
|
по дому/району): выходило «Показано 10 из 3». Тотала у той же
|
||||||
выборки в ответе нет, поэтому знаменатель убран, а не починен
|
выборки в ответе нет, поэтому знаменатель убран, а не починен
|
||||||
— разводить подпись на две выборки читается ещё хуже. */}
|
— разводить подпись на две выборки читается ещё хуже. */}
|
||||||
Показано {data.dealRows.length} фактических сделок
|
Показано {data.dealRows.length}{" "}
|
||||||
|
{pluralRu(data.dealRows.length, [
|
||||||
|
"фактическая сделка",
|
||||||
|
"фактические сделки",
|
||||||
|
"фактических сделок",
|
||||||
|
])}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
|
|
||||||
|
|
@ -14,9 +14,11 @@
|
||||||
import { render, screen } from "@testing-library/react";
|
import { render, screen } from "@testing-library/react";
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
import HistoryView from "../HistoryView";
|
||||||
import SourcesView from "../SourcesView";
|
import SourcesView from "../SourcesView";
|
||||||
import { mapResultPanel, mapSources } from "../mappers";
|
import { mapHistory, mapResultPanel, mapSources } from "../mappers";
|
||||||
import { FIXTURE_ESTIMATE } from "@/app/ui-preview/estimate/fixture";
|
import { tokens } from "../tokens";
|
||||||
|
import { FIXTURE_ESTIMATE, FIXTURE_SALES } from "@/app/ui-preview/estimate/fixture";
|
||||||
import type {
|
import type {
|
||||||
AggregatedEstimate,
|
AggregatedEstimate,
|
||||||
AnalogLot,
|
AnalogLot,
|
||||||
|
|
@ -74,17 +76,31 @@ describe("05 РЫНОК — пустая выборка сделок не рис
|
||||||
range: "—",
|
range: "—",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Цвет подписи тоже гаснет: зелёным красилась даже дельта, которой нет.
|
||||||
|
expect(data.marketDeals.kpi.deltaColor).toBeUndefined();
|
||||||
|
|
||||||
const { container } = render(<SourcesView data={data} />);
|
const { container } = render(<SourcesView data={data} />);
|
||||||
|
|
||||||
// Тот же пустой блок, что и при отсутствующем ответе street-deals.
|
|
||||||
expect(
|
|
||||||
screen.getByText(/Сделок по этому адресу за 12 месяцев не найдено/),
|
|
||||||
).toBeTruthy();
|
|
||||||
// Ни «−100% к цене объявления», ни «0,00 – 0,00» на экране быть не может.
|
// Ни «−100% к цене объявления», ни «0,00 – 0,00» на экране быть не может.
|
||||||
expect(container.textContent).not.toContain("−100");
|
expect(container.textContent).not.toContain("−100");
|
||||||
expect(container.textContent).not.toMatch(/(?<![\d,])0,00/);
|
expect(container.textContent).not.toMatch(/(?<![\d,])0,00/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("дельта красится по знаку: минус — не зелёным", () => {
|
||||||
|
// Второй причинный узел жалобы «−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, но у оценки есть свои сделки → числа из НИХ, а не прочерки", () => {
|
it("count=0, но у оценки есть свои сделки → числа из НИХ, а не прочерки", () => {
|
||||||
// Схлопывание пустой оболочки не должно гасить реальную выборку: маппер
|
// Схлопывание пустой оболочки не должно гасить реальную выборку: маппер
|
||||||
// обязан упасть на actual_deals, чьи строки видно в таблице ниже KPI.
|
// обязан упасть на actual_deals, чьи строки видно в таблице ниже KPI.
|
||||||
|
|
@ -119,8 +135,27 @@ describe("05 РЫНОК — футер таблицы сделок считае
|
||||||
|
|
||||||
render(<SourcesView data={mapSources(estimate, streetDeals)} />);
|
render(<SourcesView data={mapSources(estimate, streetDeals)} />);
|
||||||
|
|
||||||
expect(screen.getByText("Показано 3 фактических сделок")).toBeTruthy();
|
// («Показано N из M объявлений» над таблицей аналогов — законный футер
|
||||||
expect(screen.queryByText(/Показано \d+ из \d+ фактических/)).toBeNull();
|
// СВОЕЙ выборки, поэтому проверяется именно подпись под сделками.)
|
||||||
|
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(<HistoryView data={data} />);
|
||||||
|
|
||||||
|
expect(screen.getByText(/Показано 2 сделки/)).toBeTruthy();
|
||||||
|
expect(screen.queryByText(/Показано \d+ из/)).toBeNull();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1270,7 +1270,7 @@ export function mapResultPanel(
|
||||||
// та же, что #2629 дал карточке 1 над этой полосой («РЕКОМЕНДОВАННАЯ ЦЕНА
|
// та же, что #2629 дал карточке 1 над этой полосой («РЕКОМЕНДОВАННАЯ ЦЕНА
|
||||||
// ПО СДЕЛКАМ РОСРЕЕСТРА»), чтобы подпись и число читались одной парой.
|
// ПО СДЕЛКАМ РОСРЕЕСТРА»), чтобы подпись и число читались одной парой.
|
||||||
label: dealsOnlyPrice
|
label: dealsOnlyPrice
|
||||||
? ["РЕКОМЕНДОВАННЫЙ ДИАПАЗОН", "ПО СДЕЛКАМ РОСРЕЕСТРА (ДКП)"]
|
? ["РЕКОМЕНДОВАННЫЙ ДИАПАЗОН", "ПО СДЕЛКАМ РОСРЕЕСТРА"]
|
||||||
: ["ДИАПАЗОН ЦЕН В ОБЪЯВЛЕНИЯХ", "(БЕЗ УЧЁТА РЕМОНТА)"],
|
: ["ДИАПАЗОН ЦЕН В ОБЪЯВЛЕНИЯХ", "(БЕЗ УЧЁТА РЕМОНТА)"],
|
||||||
median: `${fmtMln(e.median_price_rub)} млн ₽`,
|
median: `${fmtMln(e.median_price_rub)} млн ₽`,
|
||||||
lo: `${fmtMln(e.range_low_rub)} млн`,
|
lo: `${fmtMln(e.range_low_rub)} млн`,
|
||||||
|
|
@ -2240,6 +2240,15 @@ export function mapSources(
|
||||||
// §M8 — единая формулировка дельт: всё считается «относительно цен
|
// §M8 — единая формулировка дельт: всё считается «относительно цен
|
||||||
// объявлений» (deltaPct = медиана сделок / медиана объявлений − 1).
|
// объявлений» (deltaPct = медиана сделок / медиана объявлений − 1).
|
||||||
delta: deltaPct != null ? `${fmtPct(deltaPct)} к цене объявления` : "—",
|
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),
|
range: mlnRangeSp(dealLo, dealHi),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -227,7 +227,19 @@ export interface MarketAds {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MarketDeals {
|
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: АНАЛИТИКА ДОМА ----------------------------------------------
|
// ---- OVERLAY: АНАЛИТИКА ДОМА ----------------------------------------------
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue