chore(tradein/frontend): удалить остаточный слой фикстур (#2755) (#2785)
Some checks failed
Deploy / changes (push) Successful in 18s
Deploy / build-backend (push) Has been skipped
Deploy / build-worker (push) Has been skipped
Deploy Trade-In / changes (push) Successful in 24s
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / build-frontend (push) Successful in 3m21s
Deploy Trade-In / test (push) Successful in 3m52s
Deploy / build-frontend (push) Successful in 4m39s
Deploy Trade-In / build-backend (push) Successful in 38s
Deploy / deploy (push) Successful in 1m27s
Deploy Trade-In / deploy (push) Failing after 10m5s
Some checks failed
Deploy / changes (push) Successful in 18s
Deploy / build-backend (push) Has been skipped
Deploy / build-worker (push) Has been skipped
Deploy Trade-In / changes (push) Successful in 24s
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / build-frontend (push) Successful in 3m21s
Deploy Trade-In / test (push) Successful in 3m52s
Deploy / build-frontend (push) Successful in 4m39s
Deploy Trade-In / build-backend (push) Successful in 38s
Deploy / deploy (push) Successful in 1m27s
Deploy Trade-In / deploy (push) Failing after 10m5s
This commit is contained in:
parent
90c3e7e490
commit
9de690ad4f
14 changed files with 221 additions and 761 deletions
|
|
@ -10,8 +10,8 @@
|
|||
/* ===================== SCOPED TOKENS (dark) ===================== */
|
||||
.pticaRoot[data-theme="dark"] {
|
||||
--font-ui:
|
||||
var(--font-inter), "Inter", "Manrope", -apple-system, "Segoe UI",
|
||||
system-ui, sans-serif;
|
||||
var(--font-inter), "Inter", "Manrope", -apple-system, "Segoe UI", system-ui,
|
||||
sans-serif;
|
||||
--font-mono:
|
||||
var(--font-plex-mono), "IBM Plex Mono", "Roboto Mono", ui-monospace,
|
||||
monospace;
|
||||
|
|
@ -907,31 +907,6 @@
|
|||
letter-spacing: 0.08em;
|
||||
}
|
||||
|
||||
/* ===================== PLACEHOLDER PANEL ===================== */
|
||||
.placeholderPanel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
text-align: center;
|
||||
min-height: 240px;
|
||||
border-style: dashed;
|
||||
}
|
||||
.placeholderPanel .soon {
|
||||
font-size: 22px;
|
||||
letter-spacing: 0.12em;
|
||||
color: var(--text-soft);
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.placeholderPanel p {
|
||||
font-size: 10px;
|
||||
color: var(--text-soft);
|
||||
margin: 0;
|
||||
max-width: 280px;
|
||||
}
|
||||
|
||||
/* ===================== STATE SCREENS ===================== */
|
||||
.stateScreen {
|
||||
display: grid;
|
||||
|
|
|
|||
|
|
@ -652,7 +652,7 @@ export default function TradeInV2Page() {
|
|||
|
||||
// TopNav «Мои отчёты» badge: per-user estimate count. quota.used is the
|
||||
// authoritative monthly counter; fall back to the loaded history length, then
|
||||
// to the fixture default (undefined) for unwired/pre-load states.
|
||||
// to undefined (TopNav renders 0) for pre-load states.
|
||||
const reportsCount = quota.data?.used ?? history.data?.length;
|
||||
|
||||
// Real user for the TopNav menu. undefined until /me resolves → TopNav shows
|
||||
|
|
|
|||
|
|
@ -160,7 +160,7 @@ export function CacheView({ data, onSelectRow }: CacheViewProps) {
|
|||
// #2420 — clicking/activating a row navigates to the full report
|
||||
// for that historical estimate (SectionOverlay -> page.tsx). Rows
|
||||
// stay a passive role="row"/role="cell" ARIA-grid когда onSelectRow
|
||||
// не задан (unwired/storybook usage), меняя только tabIndex/handlers
|
||||
// не задан (SectionOverlay без обработчика), меняя только tabIndex/handlers
|
||||
// — the grid structure itself is unchanged.
|
||||
const clickable = Boolean(onSelectRow);
|
||||
function activate() {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
// МЕРА v2.0.6 wordmark on the right. Static markup, id/date/validUntil via `data`.
|
||||
|
||||
import { tokens } from "./tokens";
|
||||
import { version } from "./fixtures";
|
||||
import { version } from "./ui-config";
|
||||
import type { Report } from "./types";
|
||||
|
||||
interface FooterProps {
|
||||
|
|
@ -13,7 +13,7 @@ interface FooterProps {
|
|||
// error, not a silent fallback to a fabricated report id/date.
|
||||
data: Report;
|
||||
// M4: hide the «ДЕЙСТВИТЕЛЕН ДО» validity line when there is no real estimate
|
||||
// (mirrors HeroBar). Optional → unwired/fixture usage keeps showing it.
|
||||
// (mirrors HeroBar). Optional: omitted → the line stays visible.
|
||||
hasEstimate?: boolean;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ import {
|
|||
type SetStateAction,
|
||||
} from "react";
|
||||
import { tokens } from "./tokens";
|
||||
import { dropdownOptions } from "./fixtures";
|
||||
import { dropdownOptions } from "./ui-config";
|
||||
import {
|
||||
HOUSE_TYPE_FROM_RU,
|
||||
HOUSE_TYPE_RU,
|
||||
|
|
@ -541,8 +541,8 @@ const radiusPanel: CSSProperties = {
|
|||
};
|
||||
|
||||
interface ParamsPanelProps {
|
||||
/** Called with the validated form payload on «ОЦЕНИТЬ КВАРТИРУ». Optional so
|
||||
* the still-unwired v2 page renders without props (tsc-safe). */
|
||||
/** Called with the validated form payload on «ОЦЕНИТЬ КВАРТИРУ». Optional
|
||||
* dates back to the pre-wiring page; v2/page.tsx always supplies it. */
|
||||
onSubmit?: (input: TradeInEstimateInput) => void;
|
||||
/** Disables the button + shows a pending label while the estimate is running. */
|
||||
isPending?: boolean;
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
import { useEffect, useRef } from "react";
|
||||
|
||||
import { tokens } from "./tokens";
|
||||
import { overlayNums, overlayTitles } from "./fixtures";
|
||||
import { overlayNums, overlayTitles } from "./ui-config";
|
||||
import HistoryView from "./HistoryView";
|
||||
import SourcesView from "./SourcesView";
|
||||
import AnalyticsView from "./AnalyticsView";
|
||||
|
|
@ -37,7 +37,7 @@ interface SectionOverlayProps {
|
|||
estimate?: AggregatedEstimate | null;
|
||||
// #2420 — «ПРЕДЫДУЩИЕ ОЦЕНКИ» row click: navigate to the full report for
|
||||
// that historical estimate. Optional (threaded straight to CacheView's own
|
||||
// optional onSelectRow — unwired usage leaves rows inert).
|
||||
// optional onSelectRow — without a handler rows stay inert).
|
||||
onSelectEstimate?: (id: string) => void;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -139,8 +139,9 @@ const monoMuted2: CSSProperties = {
|
|||
color: tokens.muted2,
|
||||
};
|
||||
|
||||
// Shared "open original ↗" cell style (anchor + fixture-fallback span render
|
||||
// identically; textDecoration:none keeps the anchor visually a button).
|
||||
// "Открыть оригинал ↗" cell style; textDecoration:none keeps the anchor
|
||||
// visually a button. Only a real <a> ever gets it — a row without a link
|
||||
// renders a plain dash, not a button-shaped span you cannot click.
|
||||
const linkBtn: CSSProperties = {
|
||||
fontSize: "9.5px",
|
||||
color: tokens.accent,
|
||||
|
|
@ -210,7 +211,7 @@ const AD_COLS: Col<AdRowData>[] = [
|
|||
head: <span role="columnheader">ИСТОЧНИК</span>,
|
||||
cell: (r) => (
|
||||
<span role="cell">
|
||||
<span style={badge}>{r.source ?? "Avito"}</span>
|
||||
<span style={badge}>{r.source}</span>
|
||||
</span>
|
||||
),
|
||||
empty: () => false,
|
||||
|
|
@ -263,7 +264,7 @@ const AD_COLS: Col<AdRowData>[] = [
|
|||
head: <span role="columnheader">ДИСТ.</span>,
|
||||
cell: (r) => (
|
||||
<span role="cell" style={monoMuted2}>
|
||||
{r.dist ?? "—"}
|
||||
{r.dist}
|
||||
</span>
|
||||
),
|
||||
empty: (r) => isDash(r.dist),
|
||||
|
|
@ -276,11 +277,10 @@ const AD_COLS: Col<AdRowData>[] = [
|
|||
// role="cell" sits on a display:contents wrapper so the <a> stays the grid
|
||||
// item (identical layout) AND keeps its link role inside the cell.
|
||||
cell: (r) => {
|
||||
// safeUrl gates href against javascript:/data: schemes (XSS). url is
|
||||
// undefined for the design fixtures (→ "Росреестр ↗" fallback, keeps the
|
||||
// storybook markup pixel-identical), null for a real row with no link
|
||||
// (→ "—"), and a string when a real listing URL is available.
|
||||
const href = typeof r.url === "string" ? safeUrl(r.url) : null;
|
||||
// safeUrl gates href against javascript:/data: schemes (XSS). url is a
|
||||
// string when the listing has a link and null when it has none (→ "—").
|
||||
// There is no third case: mapSources always sets the field.
|
||||
const href = r.url === null ? null : safeUrl(r.url);
|
||||
return (
|
||||
<span role="cell" style={{ display: "contents" }}>
|
||||
{href ? (
|
||||
|
|
@ -293,12 +293,8 @@ const AD_COLS: Col<AdRowData>[] = [
|
|||
>
|
||||
Источник ↗
|
||||
</a>
|
||||
) : r.url === null ? (
|
||||
<span style={monoMuted2}>—</span>
|
||||
) : (
|
||||
<span className="sv-rr" style={linkBtn}>
|
||||
Росреестр ↗
|
||||
</span>
|
||||
<span style={monoMuted2}>—</span>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
|
|
@ -336,9 +332,9 @@ const DEAL_COLS: Col<DealRowData>[] = [
|
|||
head: <span role="columnheader">ИСТОЧНИК</span>,
|
||||
cell: (r) => (
|
||||
<span role="cell">
|
||||
<span style={badge}>{r.source ?? "Росреестр"}</span>{" "}
|
||||
<span style={badge}>{r.source}</span>{" "}
|
||||
<span style={{ fontSize: "9px", color: tokens.muted2 }}>
|
||||
{r.tier ?? "по улице"}
|
||||
{r.tier}
|
||||
</span>
|
||||
</span>
|
||||
),
|
||||
|
|
@ -372,7 +368,7 @@ const DEAL_COLS: Col<DealRowData>[] = [
|
|||
head: <span role="columnheader">ДАТА</span>,
|
||||
cell: (r) => (
|
||||
<span role="cell" style={monoMuted2}>
|
||||
{r.date ?? "—"}
|
||||
{r.date}
|
||||
</span>
|
||||
),
|
||||
empty: (r) => isDash(r.date),
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
// Faithful markup port of the design header (МЕРА Оценка.dc.html, lines 42-90):
|
||||
// inline SVG logo + version + 5 nav tabs (active underline/triangle) + user menu.
|
||||
// Tabs change only local UI state via onNavigate; the user dropdown owns its
|
||||
// own useState. No data fetching — labels/version come from fixtures, the user
|
||||
// own useState. No data fetching — labels/version come from ./ui-config, the user
|
||||
// identity is fed in from the page (real useMe), colours from tokens.
|
||||
|
||||
import { useState } from "react";
|
||||
|
|
@ -13,7 +13,7 @@ import type { CSSProperties } from "react";
|
|||
import { API_BASE_URL } from "@/lib/api";
|
||||
|
||||
import { tokens } from "./tokens";
|
||||
import { navLabels, version } from "./fixtures";
|
||||
import { navLabels, version } from "./ui-config";
|
||||
import { useSupportChat } from "./SupportChatContext";
|
||||
|
||||
// Real logged-in user identity, derived by the page from useMe()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,94 @@
|
|||
// #2755 — колонка ссылки в таблице аналогов «05 РЫНОК».
|
||||
//
|
||||
// Здесь до этого жила третья ветка: когда `url` приходил `undefined`, рисовался
|
||||
// НЕкликабельный <span>, стилизованный под кнопку-ссылку «Росреестр ↗». Ветка
|
||||
// была недостижима (mapSources всегда ставит `string | null`), но выглядела как
|
||||
// рабочий элемент интерфейса. Ветку убрали, поле сделали обязательным — этот
|
||||
// тест держит обе половины: ссылка есть только там, где есть URL, а строка без
|
||||
// URL получает прочерк, а не кнопку-обманку.
|
||||
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import SourcesView from "../SourcesView";
|
||||
import { mapSources } from "../mappers";
|
||||
import { FIXTURE_ESTIMATE } from "@/app/ui-preview/estimate/fixture";
|
||||
import type { AggregatedEstimate, AnalogLot } from "@/types/trade-in";
|
||||
|
||||
function lot(over: Partial<AnalogLot>): AnalogLot {
|
||||
return {
|
||||
address: "ул. Репина, 73",
|
||||
area_m2: 54,
|
||||
rooms: 2,
|
||||
floor: 7,
|
||||
total_floors: 16,
|
||||
price_rub: 9_700_000,
|
||||
price_per_m2: 179_629,
|
||||
listing_date: "2026-05-12",
|
||||
days_on_market: 18,
|
||||
photo_url: null,
|
||||
source: "avito",
|
||||
source_url: null,
|
||||
distance_m: 120,
|
||||
tier: null,
|
||||
lat: 56.8401,
|
||||
lon: 60.5702,
|
||||
...over,
|
||||
};
|
||||
}
|
||||
|
||||
// Оффлайн-фикстура оценки репозитория с подменёнными аналогами: остальные поля
|
||||
// (sources_used, коридоры, KPI) настоящие, меняем ровно то, что проверяем.
|
||||
function estimateWith(analogs: AnalogLot[]): AggregatedEstimate {
|
||||
return {
|
||||
...FIXTURE_ESTIMATE,
|
||||
analogs,
|
||||
n_analogs: analogs.length,
|
||||
actual_deals: [],
|
||||
};
|
||||
}
|
||||
|
||||
describe("SourcesView — колонка ссылки на оригинал", () => {
|
||||
it("есть URL — кликабельная ссылка на него", () => {
|
||||
const estimate = estimateWith([
|
||||
lot({
|
||||
address: "ул. Репина, 73",
|
||||
source_url: "https://www.avito.ru/ekaterinburg/kvartiry/1",
|
||||
}),
|
||||
]);
|
||||
|
||||
render(<SourcesView data={mapSources(estimate, null)} />);
|
||||
|
||||
const link = screen.getByRole("link", { name: /Источник/ });
|
||||
expect(link).toHaveAttribute(
|
||||
"href",
|
||||
"https://www.avito.ru/ekaterinburg/kvartiry/1",
|
||||
);
|
||||
});
|
||||
|
||||
it("URL нет — прочерк, а не кнопка-обманка «Росреестр ↗»", () => {
|
||||
const estimate = estimateWith([
|
||||
lot({ address: "ул. Викулова, 33", source_url: null }),
|
||||
]);
|
||||
|
||||
render(<SourcesView data={mapSources(estimate, null)} />);
|
||||
|
||||
expect(screen.queryByText(/Росреестр ↗/)).toBeNull();
|
||||
expect(screen.queryByRole("link", { name: /Источник/ })).toBeNull();
|
||||
// Прочерк — единственное содержимое ячейки ссылки в строке аналога.
|
||||
const row = screen.getByText("ул. Викулова, 33").closest("div");
|
||||
expect(row?.textContent).toContain("—");
|
||||
});
|
||||
|
||||
it("javascript:-схема отбрасывается safeUrl — прочерк, а не ссылка", () => {
|
||||
const estimate = estimateWith([
|
||||
lot({ address: "ул. Кирова, 28", source_url: "javascript:alert(1)" }),
|
||||
]);
|
||||
|
||||
render(<SourcesView data={mapSources(estimate, null)} />);
|
||||
|
||||
expect(screen.queryByRole("link", { name: /Источник/ })).toBeNull();
|
||||
const row = screen.getByText("ул. Кирова, 28").closest("div");
|
||||
expect(row?.textContent).toContain("—");
|
||||
});
|
||||
});
|
||||
|
|
@ -1,671 +0,0 @@
|
|||
// Sample/markup data for the /trade-in/v2 "МЕРА Оценка" design port.
|
||||
// Extracted verbatim from the pixel design (МЕРА Оценка.dc.html). This is the
|
||||
// data source for the markup — components must NOT fetch; they read from here.
|
||||
// Colour fields reference tokens so the palette stays the single source of truth.
|
||||
|
||||
import { tokens } from "./tokens";
|
||||
import type {
|
||||
Analytics,
|
||||
CacheKpi,
|
||||
CacheRow,
|
||||
DkpRow,
|
||||
AdRow,
|
||||
DealRow,
|
||||
DropdownOptions,
|
||||
History,
|
||||
MarketAds,
|
||||
MarketDeals,
|
||||
ObjectInfo,
|
||||
Ranges,
|
||||
Report,
|
||||
ResultCard,
|
||||
ResultMeta,
|
||||
ScatterMini,
|
||||
SourceCard,
|
||||
Summary,
|
||||
User,
|
||||
} from "./types";
|
||||
|
||||
export const report: Report = {
|
||||
id: "9b9e2737",
|
||||
date: "20.06.2026",
|
||||
validUntil: "21.06.2026",
|
||||
};
|
||||
|
||||
export const object: ObjectInfo = {
|
||||
address: "ул. Малышева, 30",
|
||||
city: "Екатеринбург, Россия",
|
||||
area: "54",
|
||||
rooms: "2",
|
||||
floor: "5",
|
||||
totalFloors: "17",
|
||||
year: "1985",
|
||||
houseType: "Панельный",
|
||||
repair: "Хороший",
|
||||
balcony: true,
|
||||
locationIndexLabel: "+8%",
|
||||
locationIndexOk: true,
|
||||
// Approximate центр Екатеринбурга near ул. Малышева, 30 — illustrative
|
||||
// fixture coordinate for the HeroBar locator mini-map (unwired usage only).
|
||||
lat: 56.8384,
|
||||
lon: 60.6057,
|
||||
};
|
||||
|
||||
// ---- 02 RESULT ------------------------------------------------------------
|
||||
|
||||
export const resultCards: ResultCard[] = [
|
||||
{
|
||||
title: ["РЕКОМЕНДОВАННАЯ ЦЕНА", "В ОБЪЯВЛЕНИИ"],
|
||||
value: "11,29",
|
||||
unit: "млн ₽",
|
||||
range: "7,72 — 14,46 млн ₽",
|
||||
ppm: "225 801 ₽/м² · по объявлениям",
|
||||
bars: [30, 55, 80, 100, 70, 45, 25, 35],
|
||||
nav: 2,
|
||||
},
|
||||
{
|
||||
title: ["ОЖИДАЕМАЯ СДЕЛКА", "(ПОЛУЧИТЕ)"],
|
||||
value: "10,00",
|
||||
unit: "млн ₽",
|
||||
range: "6,83 — 12,81 млн ₽",
|
||||
ppm: "199 973 ₽/м²",
|
||||
delta: "−11%",
|
||||
deltaLabel: "К ОБЪЯВЛ.",
|
||||
nav: 2,
|
||||
},
|
||||
{
|
||||
title: ["ДКП · РОСРЕЕСТР", "(ФАКТИЧЕСКИЕ СДЕЛКИ)"],
|
||||
value: "9,43",
|
||||
unit: "млн ₽",
|
||||
range: "6,10 — 12,72 млн ₽",
|
||||
ppm: "188 672 ₽/м² · 44 сделки",
|
||||
bars: [25, 45, 70, 100, 85, 55, 30, 20],
|
||||
nav: 1,
|
||||
},
|
||||
];
|
||||
|
||||
export const resultMeta: ResultMeta = {
|
||||
sources: "3 / 7",
|
||||
confidence: "СРЕДНЯЯ",
|
||||
cv: "59.7%",
|
||||
};
|
||||
|
||||
export const ranges: Ranges = {
|
||||
ads: {
|
||||
label: ["ДИАПАЗОН ЦЕН В ОБЪЯВЛЕНИЯХ", "(БЕЗ УЧЁТА РЕМОНТА)"],
|
||||
median: "11,29 млн ₽",
|
||||
lo: "7,72 млн",
|
||||
hi: "14,46 млн",
|
||||
marker: { fillLeft: "8%", fillRight: "18%", dotLeft: "58%" },
|
||||
},
|
||||
deals: {
|
||||
label: ["ДИАПАЗОН ЦЕН ПО", "ФАКТИЧЕСКИМ СДЕЛКАМ"],
|
||||
median: "6,80 млн ₽",
|
||||
lo: "4,30 млн",
|
||||
hi: "19,70 млн",
|
||||
marker: { fillLeft: "4%", fillRight: "55%", dotLeft: "33%" },
|
||||
},
|
||||
};
|
||||
|
||||
// Mini "ЦЕНА × СРОК ПРОДАЖИ" scatter inside the ranges card (viewBox 168×140).
|
||||
export const scatterMini: ScatterMini = {
|
||||
deals: [
|
||||
{ x: 52, y: 96, r: 2.4 },
|
||||
{ x: 64, y: 88, r: 2.4 },
|
||||
{ x: 58, y: 101, r: 2.4 },
|
||||
{ x: 74, y: 85, r: 2.4 },
|
||||
{ x: 47, y: 103, r: 2.4 },
|
||||
{ x: 88, y: 79, r: 2.4 },
|
||||
{ x: 70, y: 93, r: 2.4 },
|
||||
{ x: 55, y: 90, r: 2.4 },
|
||||
{ x: 82, y: 99, r: 2.4 },
|
||||
{ x: 44, y: 97, r: 2.4 },
|
||||
],
|
||||
analogs: [
|
||||
{ x: 118, y: 48, r: 3 },
|
||||
{ x: 100, y: 60, r: 3 },
|
||||
{ x: 132, y: 40, r: 3 },
|
||||
{ x: 92, y: 66, r: 3 },
|
||||
{ x: 112, y: 54, r: 3 },
|
||||
{ x: 142, y: 34, r: 3 },
|
||||
],
|
||||
subject: { x: 96, y: 70, r: 3.6 },
|
||||
yTicks: [
|
||||
{ label: "15М", y: 13 },
|
||||
{ label: "10М", y: 77 },
|
||||
{ label: "4М", y: 112 },
|
||||
],
|
||||
xTicks: [
|
||||
{ label: "0", x: 34 },
|
||||
{ label: "90", x: 96 },
|
||||
{ label: "180", x: 158 },
|
||||
],
|
||||
};
|
||||
|
||||
export const sources: SourceCard[] = [
|
||||
{ name: "ЦИАН", count: "7", label: "лотов", active: true },
|
||||
{ name: "Я.НЕДВИЖИМОСТЬ", count: "1", label: "лот", active: true },
|
||||
{ name: "РОСРЕЕСТР (ВНУТР.)", count: "10", label: "лотов", active: true },
|
||||
{ name: "АВИТО ОЦЕНКА", count: "—", label: "нет данных", active: false },
|
||||
{ name: "ДОМКЛИК ПРАЙС", count: "—", label: "нет данных", active: false },
|
||||
{ name: "RESTATE", count: "—", label: "нет данных", active: false },
|
||||
];
|
||||
|
||||
// ---- 03 SUMMARY -----------------------------------------------------------
|
||||
|
||||
export const summary: Summary = {
|
||||
rows: [
|
||||
{ label: "Продажи в доме", value: "2 · 32 дн", dot: "accent", nav: 1 },
|
||||
{ label: "Аналоги", value: "23 · 6,13 млн", dot: "accent", nav: 2 },
|
||||
{ label: "Сделки", value: "10 · 5,95 млн", dot: "muted", nav: 2 },
|
||||
{ label: "Аналитика", value: "67 дн · −3%", dot: "accent", nav: 3 },
|
||||
],
|
||||
quality: { sources: "3 / 7", confidence: "средняя", cv: "59.7%" },
|
||||
};
|
||||
|
||||
// ---- OVERLAY 04: ПРОДАЖИ В ДОМЕ -------------------------------------------
|
||||
|
||||
export const dkpRows: DkpRow[] = [
|
||||
{
|
||||
area: "33.9 м²",
|
||||
price: "5 950 000 ₽",
|
||||
ppm: "175 516",
|
||||
ask: "5 272 800 ₽ за 3 дн до",
|
||||
delta: "+12.8%",
|
||||
deltaColor: tokens.danger,
|
||||
},
|
||||
{
|
||||
area: "35.4 м²",
|
||||
price: "4 900 000 ₽",
|
||||
ppm: "138 418",
|
||||
ask: "5 186 700 ₽ за 3 дн до",
|
||||
delta: "−5.5%",
|
||||
deltaColor: tokens.success,
|
||||
},
|
||||
{
|
||||
area: "36.2 м²",
|
||||
price: "4 200 000 ₽",
|
||||
ppm: "116 022",
|
||||
ask: "5 186 700 ₽ за 3 дн до",
|
||||
delta: "−19.0%",
|
||||
deltaColor: tokens.success,
|
||||
},
|
||||
{
|
||||
area: "37.0 м²",
|
||||
price: "7 503 227 ₽",
|
||||
ppm: "202 790",
|
||||
ask: "5 272 800 ₽ за 3 дн до",
|
||||
delta: "+42.3%",
|
||||
deltaColor: tokens.danger,
|
||||
},
|
||||
{
|
||||
area: "36.8 м²",
|
||||
price: "7 140 243 ₽",
|
||||
ppm: "194 028",
|
||||
ask: "5 272 800 ₽ за 3 дн до",
|
||||
delta: "+35.4%",
|
||||
deltaColor: tokens.danger,
|
||||
},
|
||||
{
|
||||
area: "42.1 м²",
|
||||
price: "6 535 050 ₽",
|
||||
ppm: "155 227",
|
||||
ask: "5 272 800 ₽ за 3 дн до",
|
||||
delta: "+23.9%",
|
||||
deltaColor: tokens.danger,
|
||||
},
|
||||
{
|
||||
area: "42.0 м²",
|
||||
price: "6 650 000 ₽",
|
||||
ppm: "158 333",
|
||||
ask: "5 781 600 ₽ за 3 дн до",
|
||||
delta: "+15.0%",
|
||||
deltaColor: tokens.danger,
|
||||
},
|
||||
{
|
||||
area: "32.5 м²",
|
||||
price: "5 362 650 ₽",
|
||||
ppm: "165 005",
|
||||
ask: "5 325 600 ₽ за 3 дн до",
|
||||
delta: "+0.7%",
|
||||
deltaColor: tokens.danger,
|
||||
},
|
||||
{
|
||||
area: "38.8 м²",
|
||||
price: "4 380 000 ₽",
|
||||
ppm: "112 887",
|
||||
ask: "5 325 600 ₽ за 3 дн до",
|
||||
delta: "−17.8%",
|
||||
deltaColor: tokens.success,
|
||||
},
|
||||
{
|
||||
area: "34.9 м²",
|
||||
price: "5 000 000 ₽",
|
||||
ppm: "143 266",
|
||||
ask: "5 254 500 ₽ за 3 дн до",
|
||||
delta: "−4.8%",
|
||||
deltaColor: tokens.success,
|
||||
},
|
||||
];
|
||||
|
||||
export const history: History = {
|
||||
houseSales: [
|
||||
{
|
||||
lot: "2-к. квартира, 50 м², 6/16 эт.",
|
||||
priceFrom: "6,25",
|
||||
priceTo: "5,98 млн ₽",
|
||||
date: "11.07.2025",
|
||||
exposure: "32 дн.",
|
||||
},
|
||||
{
|
||||
lot: "2-к. квартира, 60 м², 6/16 эт.",
|
||||
priceFrom: "7,29",
|
||||
priceTo: "7,09 млн ₽",
|
||||
date: "11.07.2025",
|
||||
exposure: "31 дн.",
|
||||
},
|
||||
],
|
||||
dkpHeader: {
|
||||
title: "ДКП-сделки на улице Академика Ландау",
|
||||
period: "24 мес",
|
||||
note: "78% сделок имеют историческую цену в объявлении · медианный торг −36.0%. Данные по улице, не по дому.",
|
||||
},
|
||||
dkpKpi: { count: "95", median: "140 066", range: "3,1 – 7,5" },
|
||||
};
|
||||
|
||||
// ---- OVERLAY 05: АНАЛОГИЧНЫЕ ОБЪЯВЛЕНИЯ -----------------------------------
|
||||
|
||||
export const adRows: AdRow[] = [
|
||||
{
|
||||
addr: "ул. Академика Ландау, 39",
|
||||
meta: "32.5 м² · 1-к · этаж 12/15",
|
||||
ppm: "170 769",
|
||||
price: "5 550 000",
|
||||
},
|
||||
{
|
||||
addr: "ул. Академика Ландау, 35А/2",
|
||||
meta: "33.5 м² · 1-к · этаж 13/25",
|
||||
ppm: "174 626",
|
||||
price: "5 850 000",
|
||||
},
|
||||
{
|
||||
addr: "ул. Академика Ландау, 38",
|
||||
meta: "32.7 м² · 1-к · этаж 10/16",
|
||||
ppm: "171 253",
|
||||
price: "5 600 000",
|
||||
},
|
||||
{
|
||||
addr: "ул. Академика Ландау, 35",
|
||||
meta: "32.7 м² · 1-к · этаж 6/12",
|
||||
ppm: "178 999",
|
||||
price: "5 853 300",
|
||||
},
|
||||
{
|
||||
addr: "ул. Академика Ландау, 35",
|
||||
meta: "37.4 м² · 1-к · этаж 8/12",
|
||||
ppm: "149 732",
|
||||
price: "5 600 000",
|
||||
},
|
||||
{
|
||||
addr: "ул. Академика Парина, 29",
|
||||
meta: "40.5 м² · 1-к · этаж 22/23",
|
||||
ppm: "160 493",
|
||||
price: "6 500 000",
|
||||
},
|
||||
{
|
||||
addr: "ул. Академика Ландау, 39",
|
||||
meta: "32.5 м² · 1-к · этаж 15/15",
|
||||
ppm: "146 153",
|
||||
price: "4 750 000",
|
||||
},
|
||||
{
|
||||
addr: "ул. Академика Ландау, 39",
|
||||
meta: "34.9 м² · 1-к · этаж 9/15",
|
||||
ppm: "143 266",
|
||||
price: "5 000 000",
|
||||
},
|
||||
{
|
||||
addr: "ул. Академика Парина, 33",
|
||||
meta: "37.1 м² · 1-к · этаж 6/14",
|
||||
ppm: "160 916",
|
||||
price: "5 970 000",
|
||||
},
|
||||
{
|
||||
addr: "ул. Академика Ландау, 34",
|
||||
meta: "41.7 м² · 1-к · этаж 22/23",
|
||||
ppm: "167 865",
|
||||
price: "6 999 999",
|
||||
},
|
||||
];
|
||||
|
||||
export const dealRows: DealRow[] = [
|
||||
{
|
||||
addr: "Екатеринбург, Академика Парина",
|
||||
meta: "38.1 м² · 1-к",
|
||||
ppm: "161 417",
|
||||
price: "6 150 000",
|
||||
},
|
||||
{
|
||||
addr: "Екатеринбург, Академика Ландау",
|
||||
meta: "33.9 м² · 1-к",
|
||||
ppm: "175 516",
|
||||
price: "5 950 000",
|
||||
},
|
||||
{
|
||||
addr: "Екатеринбург, Академика Ландау",
|
||||
meta: "39.1 м² · 1-к",
|
||||
ppm: "148 338",
|
||||
price: "5 800 000",
|
||||
},
|
||||
{
|
||||
addr: "Екатеринбург, Академика Парина",
|
||||
meta: "33.8 м² · 1-к",
|
||||
ppm: "147 929",
|
||||
price: "5 000 000",
|
||||
},
|
||||
{
|
||||
addr: "Екатеринбург, Академика Сахарова",
|
||||
meta: "37.1 м² · 1-к",
|
||||
ppm: "167 116",
|
||||
price: "6 200 000",
|
||||
},
|
||||
{
|
||||
addr: "Екатеринбург, Академика Вавилова",
|
||||
meta: "38.8 м² · 1-к",
|
||||
ppm: "165 437",
|
||||
price: "6 418 972",
|
||||
},
|
||||
{
|
||||
addr: "Екатеринбург, Академика Ландау",
|
||||
meta: "42.0 м² · 1-к",
|
||||
ppm: "158 333",
|
||||
price: "6 650 000",
|
||||
},
|
||||
{
|
||||
addr: "Екатеринбург, Академика Ландау",
|
||||
meta: "34.9 м² · 1-к",
|
||||
ppm: "143 266",
|
||||
price: "5 000 000",
|
||||
},
|
||||
{
|
||||
addr: "Екатеринбург, Академика Парина",
|
||||
meta: "32.9 м² · 1-к",
|
||||
ppm: "159 574",
|
||||
price: "5 250 000",
|
||||
},
|
||||
{
|
||||
addr: "Екатеринбург, Академика Парина",
|
||||
meta: "41.2 м² · 1-к",
|
||||
ppm: "101 942",
|
||||
price: "4 200 000",
|
||||
},
|
||||
];
|
||||
|
||||
export const marketAds: MarketAds = {
|
||||
kpi: {
|
||||
count: "23",
|
||||
median: "6,13",
|
||||
ppm: "161 351",
|
||||
range: "4,7–7,5",
|
||||
cv: "45.9%",
|
||||
},
|
||||
filters: ["Расстояние ≤ 2 км", "Планировка 1-к", "Площадь 32.5 – 41.7 м²"],
|
||||
};
|
||||
|
||||
export const marketDeals: MarketDeals = {
|
||||
kpi: {
|
||||
count: "10",
|
||||
median: "5,95",
|
||||
delta: "−3% к рынку",
|
||||
range: "4,2 – 6,7",
|
||||
},
|
||||
};
|
||||
|
||||
// ---- OVERLAY 06: АНАЛИТИКА ДОМА -------------------------------------------
|
||||
|
||||
export const analytics: Analytics = {
|
||||
header: { ads: "615 объявлений", radius: "в радиусе 300 м" },
|
||||
kpis: [
|
||||
{
|
||||
label: "СРЕДНЯЯ ЭКСПОЗИЦИЯ",
|
||||
value: "67",
|
||||
unit: "дн.",
|
||||
sub: "по архивным объявлениям",
|
||||
},
|
||||
{
|
||||
label: "СРЕДНИЙ ТОРГ",
|
||||
value: "−3.0%",
|
||||
color: tokens.success,
|
||||
sub: "от начальной до итоговой цены",
|
||||
},
|
||||
{
|
||||
label: "ДОЛЯ СНЯТЫХ",
|
||||
value: "69%",
|
||||
sub: "421 из 615",
|
||||
},
|
||||
],
|
||||
sellTimeNote: "Медиана экспозиции по архивным лотам · эталон 177 394 ₽/м²",
|
||||
sellTime: [
|
||||
{
|
||||
tier: "−5% от рынка",
|
||||
days: "~63 дн.",
|
||||
range: "обычно 18–180 дн.",
|
||||
count: "69 аналогов",
|
||||
variant: "success",
|
||||
},
|
||||
{
|
||||
tier: "По медиане",
|
||||
days: "~65 дн.",
|
||||
range: "обычно 18–182 дн.",
|
||||
count: "79 аналогов",
|
||||
variant: "accent",
|
||||
},
|
||||
{
|
||||
tier: "+5%",
|
||||
days: "~78 дн.",
|
||||
range: "обычно 16–212 дн.",
|
||||
count: "61 аналог",
|
||||
variant: "gold",
|
||||
},
|
||||
{
|
||||
tier: "+10%",
|
||||
days: "~67 дн.",
|
||||
range: "обычно 26–260 дн.",
|
||||
count: "57 аналогов",
|
||||
variant: "danger",
|
||||
},
|
||||
],
|
||||
priceHistory: {
|
||||
note: "Медиана ₽/м² по годам · только этот дом · Avito + Яндекс (596 лотов)",
|
||||
years: ["2022", "2023", "2024", "2025", "2026"],
|
||||
avito: "58,75 260,82 470,69 680,69 885,73",
|
||||
yandex: "58,75 260,82 470,51 680,42 885,24",
|
||||
yTicks: [
|
||||
{ label: "200k", y: 20 },
|
||||
{ label: "150k", y: 64 },
|
||||
{ label: "100k", y: 108 },
|
||||
{ label: "50k", y: 151 },
|
||||
{ label: "0k", y: 195 },
|
||||
],
|
||||
},
|
||||
scatterDetail: {
|
||||
note: "16 сделок и 6 аналогов в радиусе 300 м",
|
||||
deals: [
|
||||
{ x: 150, y: 210, r: 4.5 },
|
||||
{ x: 200, y: 196, r: 4.5 },
|
||||
{ x: 175, y: 222, r: 4.5 },
|
||||
{ x: 250, y: 188, r: 4.5 },
|
||||
{ x: 220, y: 204, r: 4.5 },
|
||||
{ x: 300, y: 176, r: 4.5 },
|
||||
{ x: 270, y: 200, r: 4.5 },
|
||||
{ x: 330, y: 166, r: 4.5 },
|
||||
{ x: 360, y: 182, r: 4.5 },
|
||||
{ x: 195, y: 214, r: 4.5 },
|
||||
{ x: 285, y: 190, r: 4.5 },
|
||||
{ x: 160, y: 200, r: 4.5 },
|
||||
{ x: 345, y: 198, r: 4.5 },
|
||||
{ x: 240, y: 172, r: 4.5 },
|
||||
{ x: 315, y: 186, r: 4.5 },
|
||||
{ x: 180, y: 232, r: 4.5 },
|
||||
],
|
||||
analogs: [
|
||||
{ x: 600, y: 96, r: 5.5 },
|
||||
{ x: 540, y: 124, r: 5.5 },
|
||||
{ x: 670, y: 84, r: 5.5 },
|
||||
{ x: 500, y: 142, r: 5.5 },
|
||||
{ x: 640, y: 104, r: 5.5 },
|
||||
{ x: 730, y: 70, r: 5.5 },
|
||||
],
|
||||
subject: { x: 445, y: 150, r: 6 },
|
||||
yTicks: [
|
||||
{ label: "15М", y: 24 },
|
||||
{ label: "12М", y: 81 },
|
||||
{ label: "9М", y: 138 },
|
||||
{ label: "6М", y: 195 },
|
||||
{ label: "4М", y: 252 },
|
||||
],
|
||||
xTicks: [
|
||||
{ label: "0", x: 60 },
|
||||
{ label: "45", x: 265 },
|
||||
{ label: "90", x: 470 },
|
||||
{ label: "135", x: 675 },
|
||||
{ label: "180", x: 880 },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
// ---- OVERLAY 07: ПРЕДЫДУЩИЕ ОЦЕНКИ ----------------------------------------
|
||||
|
||||
export const cacheRows: CacheRow[] = [
|
||||
{
|
||||
id: "fixture-1",
|
||||
addr: "ул. Малышева, 30",
|
||||
time: "12:45:02",
|
||||
src: "3 / 7",
|
||||
status: "свежий",
|
||||
statusColor: tokens.success,
|
||||
params: "2 комн · 54 м² · 5/9 эт.",
|
||||
},
|
||||
{
|
||||
id: "fixture-2",
|
||||
addr: "ул. Куйбышева, 48",
|
||||
time: "12:31:50",
|
||||
src: "4 / 7",
|
||||
status: "свежий",
|
||||
statusColor: tokens.success,
|
||||
params: "3 комн · 72 м² · 12/16 эт.",
|
||||
},
|
||||
{
|
||||
id: "fixture-3",
|
||||
addr: "ул. Академика Ландау, 35",
|
||||
time: "11:58:14",
|
||||
src: "2 / 7",
|
||||
status: "свежий",
|
||||
statusColor: tokens.success,
|
||||
params: "1 комн · 38 м²",
|
||||
},
|
||||
{
|
||||
id: "fixture-4",
|
||||
addr: "ул. 8 Марта, 120",
|
||||
time: "09:12:40",
|
||||
src: "3 / 7",
|
||||
status: "устаревает",
|
||||
statusColor: tokens.warn,
|
||||
params: "студия · 24 м² · 3/5 эт.",
|
||||
},
|
||||
{
|
||||
id: "fixture-5",
|
||||
addr: "пр. Ленина, 5",
|
||||
time: "вчера 18:04",
|
||||
src: "5 / 7",
|
||||
status: "устарел",
|
||||
statusColor: tokens.danger,
|
||||
params: "2 комн · 61,5 м²",
|
||||
},
|
||||
{
|
||||
id: "fixture-6",
|
||||
addr: "ул. Белинского, 86",
|
||||
time: "вчера 14:22",
|
||||
src: "2 / 7",
|
||||
status: "устарел",
|
||||
statusColor: tokens.danger,
|
||||
params: "4 комн · 95 м² · 7/10 эт.",
|
||||
},
|
||||
];
|
||||
|
||||
export const cacheKpi: CacheKpi[] = [
|
||||
{ label: "ВСЕГО ОЦЕНОК", value: "128", sub: "за последние 30 дней" },
|
||||
{
|
||||
label: "СРЕДНЯЯ ЦЕНА ОЦЕНОК",
|
||||
value: "9,6",
|
||||
unit: "млн ₽",
|
||||
sub: "по проведённым оценкам",
|
||||
},
|
||||
{ label: "ПОВТОРНЫЕ АДРЕСА", value: "18", unit: "%", sub: "доля переоценок" },
|
||||
];
|
||||
|
||||
// ---- INPUTS / DROPDOWNS ---------------------------------------------------
|
||||
|
||||
export const dropdownOptions: DropdownOptions = {
|
||||
rooms: ["1", "2", "3", "4", "5+"],
|
||||
houseType: [
|
||||
"Не указано",
|
||||
"Панельный",
|
||||
"Кирпичный",
|
||||
"Монолитный",
|
||||
"Блочный",
|
||||
"Монолитно-кирпичный",
|
||||
],
|
||||
repair: [
|
||||
"Не указано",
|
||||
"Без отделки",
|
||||
"Требует ремонта",
|
||||
"Удовлетворительный",
|
||||
"Хороший",
|
||||
"Отличный",
|
||||
],
|
||||
radius: ["300 м", "500 м", "1000 м", "2000 м"],
|
||||
crm: [
|
||||
"Не заполнять",
|
||||
"Создать сделку",
|
||||
"Привязать к контакту",
|
||||
"Экспорт в CRM",
|
||||
],
|
||||
};
|
||||
|
||||
// Note: the pre-wiring `locationFactors` fixture (fabricated 0.87 coefficient
|
||||
// + base/coef/result formula + positives/negatives) lived here — removed
|
||||
// together with the location-index rewrite. LocationDrawer now reads
|
||||
// `LocationData` produced by `mapLocation` (./mappers.ts) from the real
|
||||
// GET /trade-in/location-index response; its own default/unwired fixture is
|
||||
// LOCATION_FIXTURE in ./LocationDrawer.tsx.
|
||||
|
||||
// ---- NAV / CHROME ---------------------------------------------------------
|
||||
|
||||
export const navLabels: string[] = [
|
||||
"ОЦЕНКА",
|
||||
"ПРОДАЖИ В ДОМЕ",
|
||||
"АНАЛОГИЧНЫЕ ОБЪЯВЛЕНИЯ",
|
||||
"АНАЛИТИКА",
|
||||
"ПРЕДЫДУЩИЕ ОЦЕНКИ",
|
||||
];
|
||||
|
||||
export const overlayTitles: string[] = [
|
||||
"",
|
||||
"ПРОДАЖИ В ДОМЕ",
|
||||
"АНАЛОГИЧНЫЕ ОБЪЯВЛЕНИЯ",
|
||||
"АНАЛИТИКА ДОМА",
|
||||
"ПРЕДЫДУЩИЕ ОЦЕНКИ",
|
||||
];
|
||||
|
||||
export const overlayNums: string[] = ["", "04", "05", "06", "07"];
|
||||
|
||||
export const user: User = {
|
||||
name: "Андрей Петров",
|
||||
org: "Брусника",
|
||||
email: "a.petrov@mera.ru",
|
||||
initials: "АП",
|
||||
reports: 128,
|
||||
};
|
||||
|
||||
export const version = "v2.0.6";
|
||||
|
|
@ -1,11 +1,13 @@
|
|||
// Pure API -> presentation mappers for the /trade-in/v2 "МЕРА Оценка" HUD port.
|
||||
//
|
||||
// These functions take the real trade-in API objects (AggregatedEstimate,
|
||||
// StreetDealsResponse, HouseAnalyticsResponse) and produce the EXACT fixture
|
||||
// shapes the v2 components already consume (see ./types + ./fixtures). The Wire
|
||||
// phase swaps each component's hardcoded fixture for a `data` prop fed by one of
|
||||
// these mappers; the fixtures stay as the prop DEFAULT so unwired usage still
|
||||
// renders. ALL money/format/geometry lives here — never copy fixture literals.
|
||||
// StreetDealsResponse, HouseAnalyticsResponse) and produce the display shapes
|
||||
// the v2 components consume (see ./types). Every component's `data` prop is
|
||||
// REQUIRED and fed by one of these mappers — since #2747 there is no fixture
|
||||
// default to fall back on, and since #2755 there are no data fixtures at all
|
||||
// (./ui-config keeps only labels/options/version). ALL money/format/geometry
|
||||
// lives here; a value the API did not supply is rendered as a dash, never
|
||||
// substituted with a plausible-looking constant.
|
||||
//
|
||||
// Reconciliations / known FE-approximations (look for "TODO BE-N"):
|
||||
// BE-1 #2043 shipped 2026-07-02: AggregatedEstimate now carries real
|
||||
|
|
@ -1294,9 +1296,9 @@ export function mapSummary(
|
|||
//
|
||||
// Three pure mappers feeding the SectionOverlay views. Each returns a composite
|
||||
// presentation shape whose fields keep the exact ./types contract the views
|
||||
// already render, so the Wire phase only swaps each view's fixture import for a
|
||||
// `data` prop (fixture stays the DEFAULT). All number/format/geometry recomputed
|
||||
// here from the real API objects — never copied from fixtures. Graceful nulls
|
||||
// render. The `data` prop is required — the views had fixture imports until the
|
||||
// Wire phase and fixture DEFAULTS until #2747; both are gone. All number/format/
|
||||
// geometry is recomputed here from the real API objects. Graceful nulls
|
||||
// degrade to "—" / empty so a pending/errored sub-hook never blanks an overlay.
|
||||
//
|
||||
// NB on table number formats (faithful to the pixel design, NOT app-wide fmtPpm/
|
||||
|
|
@ -1966,26 +1968,31 @@ export function mapAnalytics(
|
|||
// ── 05 РЫНОК · АНАЛОГИ И СДЕЛКИ (SourcesView) ───────────────────────────────
|
||||
|
||||
// Real per-row source/distance/url + tier/date carried alongside the existing
|
||||
// display fields. Optional so the design fixtures (AdRow[]/DealRow[]) stay valid
|
||||
// as the component DEFAULT; the mapper always populates them.
|
||||
// display fields. REQUIRED: mapSources is the only producer of these rows and
|
||||
// always populates every field. They used to be optional so the deleted design
|
||||
// fixtures (AdRow[]/DealRow[]) would typecheck as the component DEFAULT — and
|
||||
// that optionality is what let SourcesView keep unreachable `?? "Avito"` /
|
||||
// `?? "Росреестр"` / "Росреестр ↗" fallbacks that looked like real data (#2755).
|
||||
// Keep them required: an absent value must be a TS error here, not an invented
|
||||
// brand name on screen.
|
||||
export interface AdRowData extends AdRow {
|
||||
/** Source brand label for the badge (was hardcoded "Avito"). */
|
||||
source?: string;
|
||||
/** Distance to the subject (was hardcoded "—"). */
|
||||
dist?: string;
|
||||
/** Original listing URL (was a hardcoded "Росреестр ↗" link). */
|
||||
url?: string | null;
|
||||
/** Source brand label for the badge. */
|
||||
source: string;
|
||||
/** Distance to the subject; "—" when unknown. */
|
||||
dist: string;
|
||||
/** Original listing URL; null when the row has no link. */
|
||||
url: string | null;
|
||||
/** Fix #8 — ₽/м² > OUTLIER_MULT× the pool median: excluded from spread/CV,
|
||||
* flagged with a "возможный выброс" badge instead of silently distorting it. */
|
||||
outlier?: boolean;
|
||||
outlier: boolean;
|
||||
}
|
||||
export interface DealRowData extends DealRow {
|
||||
/** Source brand label for the badge (was hardcoded "Росреестр"). */
|
||||
source?: string;
|
||||
/** Tier caption next to the badge (was hardcoded "по улице"). */
|
||||
tier?: string;
|
||||
/** Deal/listing date (was hardcoded "01.01.26"). */
|
||||
date?: string;
|
||||
/** Source brand label for the badge. */
|
||||
source: string;
|
||||
/** Tier caption next to the badge. */
|
||||
tier: string;
|
||||
/** Deal/listing date; "—" when unknown. */
|
||||
date: string;
|
||||
}
|
||||
|
||||
export interface SourcesData {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
// TypeScript shapes for the /trade-in/v2 design fixtures.
|
||||
// All values are sample/markup data (see fixtures.ts) — no API types here.
|
||||
// Display shapes rendered by the /trade-in/v2 components. Not API types — the
|
||||
// wire contract lives in @/types/trade-in and is converted here by ./mappers.
|
||||
// Originally these described the pixel-design fixtures; the fixtures are gone
|
||||
// (#2755), the shapes stayed because the mappers produce them.
|
||||
|
||||
export interface Report {
|
||||
id: string;
|
||||
|
|
@ -314,12 +316,7 @@ export interface DropdownOptions {
|
|||
// (see ./mappers.ts), which reflects the real backend contract, not the old
|
||||
// fabricated formula.
|
||||
|
||||
// ---- USER / CHROME --------------------------------------------------------
|
||||
|
||||
export interface User {
|
||||
name: string;
|
||||
org: string;
|
||||
email: string;
|
||||
initials: string;
|
||||
reports: number;
|
||||
}
|
||||
// Note: the `User` shape («Андрей Петров» / «Брусника» / reports: 128) lived
|
||||
// here for the fixture of the same name — both deleted in #2755. The real
|
||||
// logged-in identity is `TopNavUser` in ./TopNav.tsx, fed from useMe(); a
|
||||
// missing user renders the neutral «Гость», never a fabricated person.
|
||||
|
|
|
|||
62
tradein-mvp/frontend/src/components/trade-in/v2/ui-config.ts
Normal file
62
tradein-mvp/frontend/src/components/trade-in/v2/ui-config.ts
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
// Static UI chrome for the /trade-in/v2 "МЕРА Оценка" port: nav/overlay labels,
|
||||
// select options, build version. Constants only — no numbers that could be
|
||||
// mistaken for measurements.
|
||||
//
|
||||
// This file used to be `fixtures.ts` and carried ~600 lines of made-up prices,
|
||||
// deal rows and analytics lifted from the pixel design. Those were the prop
|
||||
// DEFAULTS until #2747 made every data prop required; #2755 deleted them. All
|
||||
// displayed data now comes from the API through ./mappers — if a value is
|
||||
// missing the UI shows an honest dash, never a plausible-looking number.
|
||||
|
||||
import type { DropdownOptions } from "./types";
|
||||
|
||||
// ---- INPUTS / DROPDOWNS ---------------------------------------------------
|
||||
|
||||
export const dropdownOptions: DropdownOptions = {
|
||||
rooms: ["1", "2", "3", "4", "5+"],
|
||||
houseType: [
|
||||
"Не указано",
|
||||
"Панельный",
|
||||
"Кирпичный",
|
||||
"Монолитный",
|
||||
"Блочный",
|
||||
"Монолитно-кирпичный",
|
||||
],
|
||||
repair: [
|
||||
"Не указано",
|
||||
"Без отделки",
|
||||
"Требует ремонта",
|
||||
"Удовлетворительный",
|
||||
"Хороший",
|
||||
"Отличный",
|
||||
],
|
||||
radius: ["300 м", "500 м", "1000 м", "2000 м"],
|
||||
crm: [
|
||||
"Не заполнять",
|
||||
"Создать сделку",
|
||||
"Привязать к контакту",
|
||||
"Экспорт в CRM",
|
||||
],
|
||||
};
|
||||
|
||||
// ---- NAV / CHROME ---------------------------------------------------------
|
||||
|
||||
export const navLabels: string[] = [
|
||||
"ОЦЕНКА",
|
||||
"ПРОДАЖИ В ДОМЕ",
|
||||
"АНАЛОГИЧНЫЕ ОБЪЯВЛЕНИЯ",
|
||||
"АНАЛИТИКА",
|
||||
"ПРЕДЫДУЩИЕ ОЦЕНКИ",
|
||||
];
|
||||
|
||||
export const overlayTitles: string[] = [
|
||||
"",
|
||||
"ПРОДАЖИ В ДОМЕ",
|
||||
"АНАЛОГИЧНЫЕ ОБЪЯВЛЕНИЯ",
|
||||
"АНАЛИТИКА ДОМА",
|
||||
"ПРЕДЫДУЩИЕ ОЦЕНКИ",
|
||||
];
|
||||
|
||||
export const overlayNums: string[] = ["", "04", "05", "06", "07"];
|
||||
|
||||
export const version = "v2.0.6";
|
||||
|
|
@ -79,7 +79,7 @@ export const DEFAULT_CITY: OblastCity = OBLAST_CITIES[0];
|
|||
*/
|
||||
export const UNCONFIRMED_CITY_LABEL = "Автоопределение";
|
||||
|
||||
/** RU-лейблы для дропдауна — тот же формат (string[]), что dropdownOptions.* в v2/fixtures.ts. */
|
||||
/** RU-лейблы для дропдауна — тот же формат (string[]), что dropdownOptions.* в v2/ui-config.ts. */
|
||||
export const CITY_LABELS: string[] = OBLAST_CITIES.map((c) => c.label);
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue