diff --git a/frontend/public/trade-in-v2/building.png b/frontend/public/trade-in-v2/building.png new file mode 100644 index 00000000..466bff5d Binary files /dev/null and b/frontend/public/trade-in-v2/building.png differ diff --git a/frontend/src/app/trade-in/v2/layout.tsx b/frontend/src/app/trade-in/v2/layout.tsx new file mode 100644 index 00000000..532ca966 --- /dev/null +++ b/frontend/src/app/trade-in/v2/layout.tsx @@ -0,0 +1,46 @@ +import type { Metadata } from "next"; +import { IBM_Plex_Mono, Manrope } from "next/font/google"; + +import { pageBg } from "@/components/trade-in/v2/tokens"; + +// Manrope — primary sans typeface of the МЕРА HUD. next/font is bundled +// (no package.json change). Cyrillic + latin so RU labels render correctly. +const manrope = Manrope({ + subsets: ["latin", "cyrillic"], + weight: ["200", "300", "400", "500", "600", "700"], + variable: "--font-manrope", + display: "swap", +}); + +// IBM Plex Mono — cockpit numerals / mono labels. +const plexMono = IBM_Plex_Mono({ + subsets: ["latin", "cyrillic"], + weight: ["300", "400", "500"], + variable: "--font-plex-mono", + display: "swap", +}); + +export const metadata: Metadata = { + title: "МЕРА · Оценка v2", +}; + +export default function TradeInV2Layout({ + children, +}: { + children: React.ReactNode; +}) { + return ( +
+ {children} +
+ ); +} diff --git a/frontend/src/app/trade-in/v2/page.tsx b/frontend/src/app/trade-in/v2/page.tsx new file mode 100644 index 00000000..4bd6fd4e --- /dev/null +++ b/frontend/src/app/trade-in/v2/page.tsx @@ -0,0 +1,148 @@ +"use client"; + +import { useState } from "react"; +import type { CSSProperties } from "react"; + +import { tokens } from "@/components/trade-in/v2/tokens"; +import TopNav from "@/components/trade-in/v2/TopNav"; +import HeroBar from "@/components/trade-in/v2/HeroBar"; +import ParamsPanel from "@/components/trade-in/v2/ParamsPanel"; +import ResultPanel from "@/components/trade-in/v2/ResultPanel"; +import { ObjectSummary } from "@/components/trade-in/v2/ObjectSummary"; +import { Footer } from "@/components/trade-in/v2/Footer"; +import SectionOverlay from "@/components/trade-in/v2/SectionOverlay"; +import { LocationDrawer } from "@/components/trade-in/v2/LocationDrawer"; + +// OUTER HUD FRAME + 4 corner brackets (design lines 31-37). Decorative, +// non-interactive overlay drawn over the artboard gradient. The frame has +// chamfered octagonal corners (clip-path polygon) + 4px radius; the brackets +// sit just outside it at the artboard corners. +const FRAME_INSET = 14; +const BRACKET_INSET = 6; +const BRACKET_ARM = 22; +const BRACKET_W = 1.5; + +const FRAME_CLIP = + "polygon(18px 0,calc(100% - 18px) 0,100% 18px,100% calc(100% - 18px),calc(100% - 18px) 100%,18px 100%,0 calc(100% - 18px),0 18px)"; + +const brackets: { key: string; style: CSSProperties }[] = [ + { + key: "tl", + style: { + top: BRACKET_INSET, + left: BRACKET_INSET, + borderTop: `${BRACKET_W}px solid ${tokens.bracket2}`, + borderLeft: `${BRACKET_W}px solid ${tokens.bracket2}`, + }, + }, + { + key: "tr", + style: { + top: BRACKET_INSET, + right: BRACKET_INSET, + borderTop: `${BRACKET_W}px solid ${tokens.bracket2}`, + borderRight: `${BRACKET_W}px solid ${tokens.bracket2}`, + }, + }, + { + key: "bl", + style: { + bottom: BRACKET_INSET, + left: BRACKET_INSET, + borderBottom: `${BRACKET_W}px solid ${tokens.bracket2}`, + borderLeft: `${BRACKET_W}px solid ${tokens.bracket2}`, + }, + }, + { + key: "br", + style: { + bottom: BRACKET_INSET, + right: BRACKET_INSET, + borderBottom: `${BRACKET_W}px solid ${tokens.bracket2}`, + borderRight: `${BRACKET_W}px solid ${tokens.bracket2}`, + }, + }, +]; + +export default function TradeInV2Page() { + const [nav, setNav] = useState(0); + const [drawerOpen, setDrawerOpen] = useState(false); + + return ( +
+ {/* OUTER HUD FRAME */} +
+ {/* 4 corner brackets */} + {brackets.map((b) => ( +
+ ))} + + {/* CONTENT WRAP */} +
+ + setDrawerOpen(true)} /> + +
+ + + +
+ +
+
+ + {nav !== 0 && ( + setNav(0)} + onNavigate={setNav} + /> + )} + setDrawerOpen(false)} /> +
+ ); +} diff --git a/frontend/src/components/trade-in/v2/AnalyticsView.tsx b/frontend/src/components/trade-in/v2/AnalyticsView.tsx new file mode 100644 index 00000000..50408d28 --- /dev/null +++ b/frontend/src/components/trade-in/v2/AnalyticsView.tsx @@ -0,0 +1,551 @@ +"use client"; + +// Overlay 06 "АНАЛИТИКА ДОМА" — markup port from the МЕРА Оценка pixel design. +// Data comes from fixtures (no API); the price-history dots navigate to the +// "Аналогичные объявления" overlay via onNavigate?.(2). + +import { useState } from "react"; +import { tokens } from "./tokens"; +import { analytics } from "./fixtures"; +import type { SellTimeTier } from "./types"; + +interface AnalyticsViewProps { + onNavigate?: (i: number) => void; +} + +// ---- price-history geometry ----------------------------------------------- +// Parse the SVG polyline point strings into coordinates so the markers and +// the transparent hover targets can be derived from the same source. + +interface Pt { + x: number; + y: number; +} + +function parsePoints(s: string): Pt[] { + return s + .trim() + .split(/\s+/) + .map((pair) => { + const [x, y] = pair.split(",").map(Number); + return { x, y }; + }); +} + +const avitoPts = parsePoints(analytics.priceHistory.avito); +const yandexPts = parsePoints(analytics.priceHistory.yandex); +// Яндекс only draws dots where its series diverges from Avito (последние 3 года). +const yandexDots = yandexPts.filter( + (p, i) => !avitoPts[i] || p.x !== avitoPts[i].x || p.y !== avitoPts[i].y, +); +const hoverPts: Pt[] = [...avitoPts, ...yandexDots]; + +// X positions of the year axis labels (chart scaffolding, not in fixtures). +const histYearX = [50, 250, 460, 670, 860]; + +// ---- sell-time tier tints -------------------------------------------------- +// One-off decorative tints per variant (design lines 553-556). The soft border +// hex (#bfe3d2 / #e3d2ad / #e6c3c3) and the rgba background fills are documented +// one-off design tints with no token equivalent; the label text colour reuses +// semantic tokens (success / accent / warn / danger). + +const tierStyles: Record< + SellTimeTier["variant"], + { bg: string; border: string; label: string } +> = { + success: { + bg: "rgba(27,170,107,.07)", + border: "1px solid #bfe3d2", + label: tokens.success, + }, + accent: { + bg: "rgba(46,139,255,.08)", + border: `1.5px solid ${tokens.accent}`, + label: tokens.accent, + }, + gold: { + bg: "rgba(201,154,62,.09)", + border: "1px solid #e3d2ad", + label: tokens.warn, + }, + danger: { + bg: "rgba(214,90,90,.07)", + border: "1px solid #e6c3c3", + label: tokens.danger, + }, +}; + +const cardStyle = { + background: tokens.surface.w55, + border: `1px solid ${tokens.line2}`, + borderRadius: 8, + padding: "16px 18px", +} as const; + +export default function AnalyticsView({ onNavigate }: AnalyticsViewProps) { + const [hoverHist, setHoverHist] = useState(false); + + return ( +
+ {/* header */} +
+
+ Аналитика дома +
+
+ {analytics.header.ads} · {analytics.header.radius} +
+
+ + {/* KPI row */} +
+ {analytics.kpis.map((kpi, i) => ( +
+
+ {kpi.label} +
+
+ {kpi.value} + {kpi.unit ? ( + <> + {" "} + + {kpi.unit} + + + ) : null} +
+
{kpi.sub}
+
+ ))} +
+ + {/* срок продажи в зависимости от цены */} +
+
+ Срок продажи в зависимости от цены +
+
+ {analytics.sellTimeNote} +
+
+ {analytics.sellTime.map((tier, i) => { + const v = tierStyles[tier.variant]; + return ( +
+
+ {tier.tier} +
+
+ {tier.days} +
+
+ {tier.range} +
+ {tier.count} +
+
+ ); + })} +
+
+ + {/* история цен */} +
+
+
+
+ История цен в этом доме +
+
+ {analytics.priceHistory.note} +
+
+
+ + + Avito + + + + Яндекс + +
+
+ + + + + + + + + + + 200k + + + 150k + + + 100k + + + 50k + + + 0k + + + + {analytics.priceHistory.years.map((yr, i) => ( + + {yr} + + ))} + + + + {/* white dot fill on an accent ring — documented one-off, no token (design l.568) */} + + Архивные объявления Avito · нажмите, чтобы открыть + {avitoPts.map((p, i) => ( + + ))} + + + {yandexDots.map((p, i) => ( + + ))} + + setHoverHist(true)} + onMouseLeave={() => setHoverHist(false)} + onClick={() => onNavigate?.(2)} + > + {hoverPts.map((p, i) => ( + + ))} + + + {hoverHist ? ( + // Dark navy tooltip surface + white text — documented one-off literals + // matching the design (l.572); no token equivalent. +
+
+ Архивные объявления этого дома +
+
+ Нажмите, чтобы открыть список → +
+
+ ) : null} +
+ + {/* цена × срок продажи (детально) */} +
+
+
+
+ Цена × срок продажи +
+
+ Зависимость цены от срока экспозиции ·{" "} + {analytics.scatterDetail.note} +
+
+
+ + + аналоги + + + + сделки + + + + объект + +
+
+ + + + + + + + + + + + + + + + {analytics.scatterDetail.deals.map((p, i) => ( + + ))} + + + {analytics.scatterDetail.analogs.map((p, i) => ( + + ))} + + + + + {analytics.scatterDetail.yTicks.map((t) => ( + + {t.label} + + ))} + + + {analytics.scatterDetail.xTicks.map((t) => ( + + {t.label} + + ))} + + + СРОК ПРОДАЖИ, ДН + + + ЦЕНА ПРОДАЖИ, ₽ + + +
+
+ ); +} diff --git a/frontend/src/components/trade-in/v2/CacheView.tsx b/frontend/src/components/trade-in/v2/CacheView.tsx new file mode 100644 index 00000000..1d8bd778 --- /dev/null +++ b/frontend/src/components/trade-in/v2/CacheView.tsx @@ -0,0 +1,147 @@ +// OVERLAY 07: ПРЕДЫДУЩИЕ ОЦЕНКИ (cache view). +// Faithful markup port of МЕРА Оценка.dc.html lines 593-613. +// Data from fixtures (cacheKpi + cacheRows); no API, no local state. + +import { tokens } from "./tokens"; +import { cacheKpi, cacheRows } from "./fixtures"; + +const GRID_COLUMNS = "2.4fr 1fr 1fr 1fr"; + +export function CacheView() { + return ( +
+ {/* 3 KPI cards */} +
+ {cacheKpi.map((kpi) => ( +
+
+ {kpi.label} +
+
+ {kpi.value} + {kpi.unit && ( + <> + {kpi.unit !== "%" && " "} + + {kpi.unit} + + + )} +
+
{kpi.sub}
+
+ ))} +
+ + {/* Последние оценки table */} +
+
+ Последние оценки +
+
+ АДРЕС + ВРЕМЯ + ИСТОЧНИКОВ + СТАТУС +
+ {cacheRows.map((r, i) => ( +
+ {r.addr} + + {r.time} + + + {r.src} + + + + {r.status} + +
+ ))} +
+
+ ); +} diff --git a/frontend/src/components/trade-in/v2/Footer.tsx b/frontend/src/components/trade-in/v2/Footer.tsx new file mode 100644 index 00000000..e4e05182 --- /dev/null +++ b/frontend/src/components/trade-in/v2/Footer.tsx @@ -0,0 +1,123 @@ +// Report footer for the /trade-in/v2 "МЕРА Оценка" design port. +// Faithful markup port of the design footer (МЕРА Оценка.dc.html, lines 426-439): +// report id / date / valid-until on the left, a decorative centre line, and the +// МЕРА v2.0.6 wordmark on the right. Static markup, data from fixtures. + +import { tokens } from "./tokens"; +import { report, version } from "./fixtures"; + +export function Footer() { + return ( +
+
+ + ОТЧЁТ {report.id} + + + ДАТА {report.date} + + + ДЕЙСТВИТЕЛЕН ДО{" "} + {report.validUntil} + +
+ +
+ + +
+ + +
+ +
+ + + МЕРА + + + {version} + +
+
+ ); +} diff --git a/frontend/src/components/trade-in/v2/HeroBar.tsx b/frontend/src/components/trade-in/v2/HeroBar.tsx new file mode 100644 index 00000000..ef512725 --- /dev/null +++ b/frontend/src/components/trade-in/v2/HeroBar.tsx @@ -0,0 +1,484 @@ +"use client"; + +import { tokens } from "./tokens"; +import { object, report } from "./fixtures"; + +interface HeroBarProps { + onOpenInfo: () => void; +} + +export default function HeroBar({ onOpenInfo }: HeroBarProps) { + return ( +
+ + + {/* LEFT: meta + buttons */} +
+
+
+
+ ОТЧЁТ +
+
+ {report.id} +
+
+
+
+ ДАТА +
+
+ {report.date} +
+
+
+
+ ДЕЙСТВИТЕЛЕН ДО +
+
+ {report.validUntil} +
+
+
+ +
+ + +
+
+ + {/* RIGHT: photo + overlays */} +
+ +
+
+
+ + {/* address card */} +
+
+ АДРЕС +
+
+ {object.address} +
+ + {object.city} + +
+
+
+ {object.streetView} +
+
+
+ + КОЭФ. ЛОКАЦИИ + + + + {object.locationCoef} + + + ? + + +
+
+ + {/* compass */} +
+ + + + + + {object.compass} + +
+ + {/* distance scale */} +
+
+ + + + + +
+
+ 0 + 25 + 50 + 75 + 100м +
+
+ + {/* corner brackets */} +
+
+
+
+
+
+ ); +} diff --git a/frontend/src/components/trade-in/v2/HistoryView.tsx b/frontend/src/components/trade-in/v2/HistoryView.tsx new file mode 100644 index 00000000..ca0fbeed --- /dev/null +++ b/frontend/src/components/trade-in/v2/HistoryView.tsx @@ -0,0 +1,359 @@ +// Overlay 04 "ПРОДАЖИ В ДОМЕ" body for the /trade-in/v2 "МЕРА Оценка" port. +// Faithful markup port of the design's showHistory block (МЕРА Оценка.dc.html, +// lines 457-489): "История продаж в этом доме" card (house sales) + +// "ДКП-сделки на улице" card (3 KPI + dealine table + footer count). +// Static markup — data comes from fixtures, no fetch / no local state. + +import { tokens } from "./tokens"; +import { history, dkpRows } from "./fixtures"; + +// Shared grid templates (kept verbatim from the design column tracks). +const houseGrid = "2fr 1.4fr 1fr 1fr"; +const dkpGrid = "2.2fr 1fr 1.3fr 1fr .9fr 2fr"; + +export default function HistoryView() { + return ( +
+ {/* ── История продаж в этом доме ───────────────────────────── */} +
+
+ + История продаж в этом доме + + + {history.houseSales.length} ЛОТА + +
+ +
+ ЛОТ + ЦЕНА НАЧ. → ПОСЛ. + ДАТА + ЭКСПОЗИЦИЯ +
+ + {history.houseSales.map((row, i) => ( +
+ {row.lot} + + {row.priceFrom} →{" "} + {row.priceTo} + + + {row.date} + + + {row.exposure} + +
+ ))} +
+ + {/* ── ДКП-сделки на улице ──────────────────────────────────── */} +
+
+
+
+
+ ПОХОЖИЕ КВАРТИРЫ · ПО ВАШЕЙ УЛИЦЕ +
+
+ {history.dkpHeader.title} +
+
+
+ Период:{" "} + {history.dkpHeader.period} +
+ Источник: Росреестр + объявления +
+
+
+ {history.dkpHeader.note} +
+
+ + {/* KPI band */} +
+
+
+ СДЕЛОК ЗА 24 МЕС +
+
+ {history.dkpKpi.count} +
+
+
+
+ МЕДИАНА +
+
+ {history.dkpKpi.median}{" "} + + ₽/м² + +
+
+
+
+ ДИАПАЗОН +
+
+ {history.dkpKpi.range}{" "} + + млн ₽ + +
+
+
+ + {/* Table header */} +
+ АДРЕС + ПЛОЩАДЬ + ЦЕНА СДЕЛКИ + ₽/М² + ДАТА + ОБЪЯВЛЕНИЕ ДО СДЕЛКИ +
+ + {dkpRows.map((r, i) => ( +
+ + Екатеринбург, Академика Ландау + + + {r.area} + + + {r.price} + + + {r.ppm} + + + 01.01.26 + + + + Яндекс + + {r.ask} + {r.delta} + +
+ ))} + +
+ Показано {dkpRows.length} из {history.dkpKpi.count} сделок +
+
+
+ ); +} diff --git a/frontend/src/components/trade-in/v2/LocationDrawer.tsx b/frontend/src/components/trade-in/v2/LocationDrawer.tsx new file mode 100644 index 00000000..290ece6a --- /dev/null +++ b/frontend/src/components/trade-in/v2/LocationDrawer.tsx @@ -0,0 +1,330 @@ +"use client"; + +// "КОЭФФИЦИЕНТ ЛОКАЦИИ" right-side drawer for the /trade-in/v2 "МЕРА Оценка" +// design port. Faithful markup port of the design drawer (МЕРА Оценка.dc.html, +// lines 618-660): scrim + 452px right slider that slides in via translateX, a +// big location coefficient, intro, base×coef formula, the factors that raise / +// lower price, and a geo-source footer. Static markup, data from fixtures. +// Open/close is driven entirely by props (no internal state, no fetch). + +import { tokens } from "./tokens"; +import type { LocationFactor } from "./types"; +import { locationFactors } from "./fixtures"; + +interface LocationDrawerProps { + open: boolean; + onClose: () => void; +} + +function FactorRow({ + factor, + variant, +}: { + factor: LocationFactor; + variant: "pos" | "neg"; +}) { + const isPos = variant === "pos"; + return ( +
+ + + {isPos ? "+" : "−"} + + {factor.label} + + + {factor.delta} + +
+ ); +} + +export function LocationDrawer({ open, onClose }: LocationDrawerProps) { + return ( + <> + + + {/* scrim */} +
+ + {/* drawer */} +
+ {/* left accent rail */} +
+ {/* corner brackets */} +
+
+ + {/* header */} +
+
+
+ ПОЯСНЕНИЕ К РАСЧЁТУ +
+
+ КОЭФФИЦИЕНТ ЛОКАЦИИ +
+
+
+ ✕ +
+
+ + {/* big coefficient */} +
+ + {locationFactors.coef} + +
+ множитель к базовой +
+ цене по городу +
+
+ + {/* intro */} +
+ Показывает, как адрес корректирует стоимость относительно медианы по + Екатеринбургу. 1.00 — средний + уровень. 0.87 означает, что + локация снижает цену примерно на{" "} + 13% из-за баланса факторов ниже. +
+ + {/* formula */} +
+ + {locationFactors.formula.base} + + × + + {locationFactors.formula.coef} + + = + + {locationFactors.formula.result} + +
+ + {/* positives */} +
+ ЧТО ПОВЫШАЕТ ЦЕНУ +
+
+ {locationFactors.positives.map((f, i) => ( + + ))} +
+ + {/* negatives */} +
+ ЧТО СНИЖАЕТ ЦЕНУ +
+
+ {locationFactors.negatives.map((f, i) => ( + + ))} +
+ + {/* footer source */} +
+ {locationFactors.footer} +
+
+ + ); +} diff --git a/frontend/src/components/trade-in/v2/ObjectSummary.tsx b/frontend/src/components/trade-in/v2/ObjectSummary.tsx new file mode 100644 index 00000000..7c58c339 --- /dev/null +++ b/frontend/src/components/trade-in/v2/ObjectSummary.tsx @@ -0,0 +1,302 @@ +// 03 СВОДКА ОБЪЕКТА — right-rail summary card for the /trade-in/v2 "МЕРА Оценка" +// design port. Faithful markup port of МЕРА Оценка.dc.html lines 397-422: +// address + "В РАСЧЁТЕ" badge, four clickable per-section totals, and a +// "КАЧЕСТВО ДАННЫХ" block with a donut. Data from fixtures; clicks fire +// onNavigate(row.nav) only — no API, no fetching. + +"use client"; + +import { tokens } from "./tokens"; +import { object, summary } from "./fixtures"; + +interface ObjectSummaryProps { + onNavigate: (i: number) => void; +} + +export function ObjectSummary({ onNavigate }: ObjectSummaryProps) { + return ( +
+ + + {/* HUD corner bracket */} +
+ + {/* Header: 03 СВОДКА ОБЪЕКТА + В РАСЧЁТЕ badge */} +
+
+ + 03 + + + СВОДКА ОБЪЕКТА + +
+ + + В РАСЧЁТЕ + +
+ +
+ ИТОГИ ПО РАЗДЕЛАМ +
+ + {/* Address */} +
+ + + + +
+
{object.address}
+
+ {object.city} +
+
+ + + +
+ + {/* Per-section totals (clickable) */} +
+ {summary.rows.map((row, i) => ( +
onNavigate(row.nav)} + style={{ + display: "flex", + justifyContent: "space-between", + alignItems: "center", + padding: "9px 6px", + borderBottom: + i < summary.rows.length - 1 + ? `1px dotted ${tokens.lineGradient}` + : undefined, + cursor: "pointer", + borderRadius: 5, + }} + > + + + {row.label} + + + + {row.value} + + + +
+ ))} +
+ + {/* КАЧЕСТВО ДАННЫХ */} +
+ + + + + + +
+
+ КАЧЕСТВО ДАННЫХ +
+
+ Источников + + {summary.quality.sources} + +
+
+ Достоверность + + {summary.quality.confidence} + +
+
+ CV + + {summary.quality.cv} + +
+
+
+
+ ); +} diff --git a/frontend/src/components/trade-in/v2/ParamsPanel.tsx b/frontend/src/components/trade-in/v2/ParamsPanel.tsx new file mode 100644 index 00000000..04f068f8 --- /dev/null +++ b/frontend/src/components/trade-in/v2/ParamsPanel.tsx @@ -0,0 +1,958 @@ +"use client"; + +// 01 ПАРАМЕТРЫ КВАРТИРЫ — left card of the /trade-in/v2 "МЕРА Оценка" port. +// Faithful markup port (МЕРА Оценка.dc.html lines 146-240). Markup-first: all +// values come from fixtures (no API). Dropdowns / balcony toggle drive local UI +// state only. Hover/active + @keyframes live in a pp-prefixed local + + {/* HUD corner brackets */} +
+
+ + {/* header */} +
+
+ + 01 + + + ПАРАМЕТРЫ КВАРТИРЫ + +
+ + ШАГ 1/1 + +
+ + {/* MAP */} +
+ + + + + + + + + + + + + + + + + + + + + + + + + {/* center pin */} +
+
+ + + {object.address} + +
+
+
+ + {/* analog price pins */} +
+ 12,7 млн ₽
+ 54 м² +
+
+ 7,6 млн ₽
+ 44 м² +
+
+ 22,4 млн ₽
+ 55 м² +
+
+ 15,9 млн ₽
+ 55 м² +
+
+ 14,9 млн ₽
+ 56 м² +
+ + {/* analog dots */} + + + + + {/* map controls */} +
+
+ + +
+
+ − +
+
+ + + + + + + +
+
+ + {/* map corner bracket */} +
+
+ + {/* radius row */} +
+ + РАДИУС АНАЛИЗА + +
+
toggle("radius")} + style={{ + display: "flex", + alignItems: "center", + gap: 8, + background: tokens.surface.w60, + border: `1px solid ${tokens.line}`, + borderRadius: 5, + padding: "6px 10px", + fontSize: 12, + fontFamily: tokens.font.mono, + cursor: "pointer", + }} + > + {radius} + +
+ {openDd === "radius" && ( +
+ {dropdownOptions.radius.map((o) => ( +
pick(setRadius)(o)} + style={{ + padding: "7px 10px", + fontSize: 13, + fontFamily: tokens.font.mono, + borderRadius: 4, + cursor: "pointer", + color: o === radius ? tokens.accent : tokens.ink, + fontWeight: o === radius ? 600 : 400, + }} + > + {o} +
+ ))} +
+ )} +
+
+ + + Объявления + + + + Сделки + +
+
+ + {/* INPUTS */} +
+
+
АДРЕС (YANDEX / NOMINATIM)
+
+
+ {object.address} · Куйбышева, 48… +
+ + + + + + + +
+
+ +
+
+
ПЛОЩАДЬ, М²
+
{object.area}
+
+
+
КОМНАТ
+
toggle("rooms")} + value={rooms} + options={dropdownOptions.rooms} + onSelect={pick(setRooms)} + mono + triggerFontSize={14} + optionFontSize={13} + /> +
+
+ +
+
+
+ ЭТАЖ + если знаешь +
+
{object.floor}
+
+
+
+ ВСЕГО ЭТАЖЕЙ + если знаешь +
+
{object.totalFloors}
+
+
+ +
+ 1-й и последний этаж снижают цену на 5–10% — заполни если знаешь +
+ +
+
+
+ ГОД ПОСТРОЙКИ + опц. +
+
{object.year}
+
+
+
+ ТИП ДОМА + опц. +
+
toggle("houseType")} + value={houseType} + options={dropdownOptions.houseType} + onSelect={pick(setHouseType)} + mono={false} + triggerFontSize={13} + optionFontSize={12} + /> +
+
+ +
+
+
+ СОСТОЯНИЕ РЕМОНТА + опц. +
+
toggle("repair")} + value={repair} + options={dropdownOptions.repair} + onSelect={pick(setRepair)} + mono={false} + triggerFontSize={13} + optionFontSize={12} + /> +
+
+
БАЛКОН / ЛОДЖИЯ
+
+
setBalcony(false)} + style={{ ...balBase, ...(balcony ? balOff : balOn) }} + > + НЕТ +
+
setBalcony(true)} + style={{ ...balBase, ...(balcony ? balOn : balOff) }} + > + ДА +
+
+
+
+ +
+
+ CRM — ДЛЯ МЕНЕДЖЕРА + опц. +
+
toggle("crm")} + value={crm} + options={dropdownOptions.crm} + onSelect={pick(setCrm)} + mono={false} + triggerFontSize={12} + optionFontSize={12} + variant="dashed" + openUp + triggerHeight={34} + triggerBg={tokens.surface.w50} + triggerColor={tokens.hint} + caretColor={tokens.muted4} + /> +
+
+ + {/* button */} + + + {/* cache status */} +
+ КЭШ ПО АДРЕСУ — 24 Ч + + + ГОТОВ + +
+
+ ); +} diff --git a/frontend/src/components/trade-in/v2/ResultPanel.tsx b/frontend/src/components/trade-in/v2/ResultPanel.tsx new file mode 100644 index 00000000..c8020449 --- /dev/null +++ b/frontend/src/components/trade-in/v2/ResultPanel.tsx @@ -0,0 +1,880 @@ +"use client"; + +import { Fragment } from "react"; +import { tokens } from "./tokens"; +import { + ranges, + resultCards, + resultMeta, + scatterMini, + sources, +} from "./fixtures"; + +const { + accent, + onAccent, + accentGlow, + ink, + muted, + muted2, + muted3, + line, + line2, + line3, + bracket, + barLo, + barMid, + track, + success, + hint, + scatterDeal, + disabledBorder, + disabledValue, + disabledLabel, + surface, + font, +} = tokens; + +interface ResultPanelProps { + onNavigate: (i: number) => void; +} + +// Mini-histogram column tint: peak column = accent, its neighbours = mid, rest = lo. +function barColor(i: number, bars: number[]): string { + const peak = bars.indexOf(Math.max(...bars)); + if (i === peak) return accent; + if (i === peak - 1 || i === peak + 1) return barMid; + return barLo; +} + +// Render a string[] with
between lines (faithful to the design line breaks). +function lines(parts: string[]) { + return parts.map((part, i) => ( + + {i > 0 &&
} + {part} +
+ )); +} + +// Split a source name on its parenthetical so the design's forced +// "РОСРЕЕСТР / (ВНУТР.)" two-line break (line-height 1.3) is reproduced; plain +// names without " (" stay on a single line. +function sourceName(name: string) { + const idx = name.indexOf(" ("); + if (idx === -1) return name; + return ( + <> + {name.slice(0, idx)} +
+ {name.slice(idx + 1)} + + ); +} + +export default function ResultPanel({ onNavigate }: ResultPanelProps) { + return ( +
+ + + {/* header */} +
+
+ + 02 + + + РЕЗУЛЬТАТ ОЦЕНКИ + +
+
+ + ИСТОЧНИКОВ:{" "} + + {resultMeta.sources} + + + + ДОСТОВЕРНОСТЬ: {resultMeta.confidence} + + + CV{" "} + + {resultMeta.cv} + + +
+
+ + {/* result cards */} +
+ {resultCards.map((card, ci) => ( +
+
+
+ {lines(card.title)} +
+
+ + {card.value} + + + {card.unit} + +
+
+ {card.range} +
+
+ {card.ppm} +
+ + {card.bars ? ( +
+
+ {card.bars.map((h, bi) => ( + + ))} +
+
onNavigate(card.nav)} + style={{ + display: "flex", + alignItems: "center", + gap: 4, + cursor: "pointer", + fontSize: 9, + letterSpacing: 0.3, + color: accent, + whiteSpace: "nowrap", + }} + > + Подробнее +
+
+ ) : ( + <> +
onNavigate(card.nav)} + style={{ + marginTop: 16, + display: "flex", + alignItems: "center", + justifyContent: "flex-end", + gap: 4, + cursor: "pointer", + fontSize: 9, + letterSpacing: 0.3, + color: accent, + }} + > + Подробнее +
+
+ + + + +
+ + {card.delta} + + + {card.deltaLabel} + +
+
+ + )} +
+ ))} +
+ + {/* ranges + radar */} +
+
+ + {/* left range — ads */} +
+
+ {lines(ranges.ads.label)} +
+
+ медиана · {ranges.ads.median} +
+
+
+
+
+
+ {ranges.ads.lo} + {ranges.ads.median} + {ranges.ads.hi} +
+
+ + {/* radar — price × sell-time scatter */} +
+
+ ЦЕНА × СРОК ПРОДАЖИ +
+ + + + + + + + + + + {scatterMini.deals.map((p, i) => ( + + ))} + + + {scatterMini.analogs.map((p, i) => ( + + ))} + + + + + {scatterMini.yTicks.map((t, i) => ( + + {t.label} + + ))} + + + {scatterMini.xTicks.map((t, i) => ( + + {t.label} + + ))} + + + СРОК ПРОДАЖИ, ДН + + + ЦЕНА ПРОДАЖИ + + +
+ + + аналоги + + + + сделки + + + + объект + +
+
onNavigate(3)} + style={{ + display: "flex", + alignItems: "center", + gap: 4, + cursor: "pointer", + fontSize: 9, + letterSpacing: 0.3, + color: accent, + marginTop: 2, + }} + > + Подробнее +
+
+ + {/* right range — deals */} +
+
+ {lines(ranges.deals.label)} +
+
+ медиана · {ranges.deals.median} +
+
+
+
+
+
+ {ranges.deals.lo} + {ranges.deals.median} + {ranges.deals.hi} +
+
+
+ + {/* sources */} +
+
+ + ИСТОЧНИКИ ДАННЫХ + + onNavigate(2)} + style={{ + display: "flex", + alignItems: "center", + gap: 4, + cursor: "pointer", + fontSize: 9.5, + letterSpacing: 0.3, + color: accent, + }} + > + Подробнее + +
+
+ {sources.map((s, si) => + s.active ? ( +
+
+ {sourceName(s.name)} +
+
+ {s.count} +
+
+ + {s.label} +
+
+ ) : ( +
+
+ {s.name} +
+
+ {s.count} +
+
+ {s.label} +
+
+ ), + )} +
+
+ + При недоступности источника частичный результат не блокируется + + + ПОСТРОЕНО ПО 6 АНАЛОГАМ И 10 СДЕЛКАМ ЗА 6 МЕСЯЦЕВ + +
+
+
+ ); +} diff --git a/frontend/src/components/trade-in/v2/SectionOverlay.tsx b/frontend/src/components/trade-in/v2/SectionOverlay.tsx new file mode 100644 index 00000000..7f7e465f --- /dev/null +++ b/frontend/src/components/trade-in/v2/SectionOverlay.tsx @@ -0,0 +1,150 @@ +"use client"; + +// Overlay wrapper for the nav-driven section panels of the /trade-in/v2 +// "МЕРА Оценка" design. Faithful port of the design overlay container +// (МЕРА Оценка.dc.html lines 443-453 + 615-616): a glass/blur absolute layer +// with HUD corner brackets, a header (section number + title + "← К ОЦЕНКЕ" +// button) and a scrollable body that swaps the active view. + +import { tokens } from "./tokens"; +import { overlayNums, overlayTitles } from "./fixtures"; +import HistoryView from "./HistoryView"; +import SourcesView from "./SourcesView"; +import AnalyticsView from "./AnalyticsView"; +import { CacheView } from "./CacheView"; + +interface SectionOverlayProps { + active: number; + onClose: () => void; + onNavigate: (i: number) => void; +} + +export default function SectionOverlay({ + active, + onClose, + onNavigate, +}: SectionOverlayProps) { + return ( +
+ + + {/* HUD corner brackets */} +
+
+ + {/* overlay header */} +
+
+ + {overlayNums[active]} + + + {overlayTitles[active]} + +
+
+ + + К ОЦЕНКЕ + +
+
+ + {/* overlay body */} +
+ {active === 1 && } + {active === 2 && } + {active === 3 && } + {active === 4 && } +
+
+ ); +} diff --git a/frontend/src/components/trade-in/v2/SourcesView.tsx b/frontend/src/components/trade-in/v2/SourcesView.tsx new file mode 100644 index 00000000..9ea730a7 --- /dev/null +++ b/frontend/src/components/trade-in/v2/SourcesView.tsx @@ -0,0 +1,401 @@ +import type { CSSProperties } from "react"; + +import { tokens } from "./tokens"; +import { adRows, dealRows, marketAds, marketDeals } from "./fixtures"; +import type { AdRow, DealRow } from "./types"; + +// Overlay 05 — РЫНОК · АНАЛОГИ И СДЕЛКИ. +// Faithful markup port from "МЕРА Оценка.dc.html" (lines 492-537). +// Card A: объявления в продаже (3 KPI + filter chips + adRows table). +// Card B: фактические сделки (3 KPI + dealRows table). +// Static markup: data from fixtures, no fetching, no interactive state. + +// One-off tints not present in the v2 palette (kept literal, faithful to design). +const KPI_BG = "rgba(238,244,250,.5)"; +const BADGE_BG = "#eaf1f8"; +const AVITO_DOT = "#e8743b"; + +const AD_GRID = "2.4fr 1fr 1fr 1.2fr .7fr 1fr"; +const DEAL_GRID = "2.6fr 1.2fr 1fr 1.2fr 1fr"; + +const card: CSSProperties = { + background: tokens.surface.w55, + border: `1px solid ${tokens.line2}`, + borderRadius: 8, + overflow: "hidden", +}; + +const cardHeader: CSSProperties = { + display: "flex", + alignItems: "flex-start", + justifyContent: "space-between", + padding: "14px 18px", + borderBottom: `1px solid ${tokens.lineSoft}`, +}; + +const eyebrow: CSSProperties = { + fontSize: "8.5px", + letterSpacing: "2px", + color: tokens.muted2, + marginBottom: 5, +}; + +const cardTitle: CSSProperties = { + fontSize: "12px", + fontWeight: 600, + color: tokens.ink2, +}; + +const headerRight: CSSProperties = { + textAlign: "right", + fontSize: "9.5px", + color: tokens.muted2, + lineHeight: 1.6, +}; + +const kpiRow: CSSProperties = { + display: "grid", + gridTemplateColumns: "repeat(3,1fr)", + borderBottom: `1px solid ${tokens.lineSoft}`, + background: KPI_BG, +}; + +const kpiLabel: CSSProperties = { + fontSize: "9px", + letterSpacing: "1.5px", + color: tokens.muted2, +}; + +const tableHead: CSSProperties = { + gap: "10px", + padding: "9px 18px", + fontSize: "9px", + letterSpacing: "1px", + color: tokens.muted2, + borderBottom: `1px solid ${tokens.lineSoft2}`, +}; + +const tableRow: CSSProperties = { + gap: "10px", + padding: "11px 18px", + fontSize: "11px", + borderBottom: `1px solid ${tokens.lineSoft3}`, + alignItems: "center", +}; + +const metaSpan: CSSProperties = { + color: tokens.muted2, + fontFamily: tokens.font.mono, + fontSize: "9.5px", +}; + +const badge: CSSProperties = { + fontSize: "9px", + background: BADGE_BG, + border: `1px solid ${tokens.line2}`, + borderRadius: 3, + padding: "2px 8px", + color: tokens.muted, +}; + +const chip: CSSProperties = { + fontSize: "10px", + color: tokens.muted, + border: `1px solid ${tokens.line2}`, + borderRadius: 20, + padding: "3px 10px", +}; + +const monoMuted: CSSProperties = { + fontFamily: tokens.font.mono, + color: tokens.muted, +}; + +const monoInk: CSSProperties = { + fontFamily: tokens.font.mono, + color: tokens.ink2, +}; + +const monoMuted2: CSSProperties = { + fontFamily: tokens.font.mono, + color: tokens.muted2, +}; + +function KpiCell({ + label, + value, + unit, + valueColor = tokens.ink2, + unitColor = tokens.muted, + last = false, +}: { + label: string; + value: string; + unit: string; + valueColor?: string; + unitColor?: string; + last?: boolean; +}) { + return ( +
+
{label}
+
+ {value}{" "} + {unit} +
+
+ ); +} + +export default function SourcesView() { + return ( +
+ + + {/* объявления */} +
+
+
+
ПОХОЖИЕ КВАРТИРЫ · В ПРОДАЖЕ
+
+ Объявления — аналогичные квартиры в продаже +
+
+
+ Обновлено: —
+ Источников: 2 / 7 +
+
+ +
+ + + +
+ +
+ + ФИЛЬТР + + {marketAds.filters.map((f, i) => ( + + {f} + + ))} + + + Avito · 10 + +
+ +
+ АДРЕС + ИСТОЧНИК + ₽/М² + СТОИМОСТЬ + ДИСТ. + +
+ + {adRows.map((r: AdRow, i) => ( +
+ + {r.addr} + + {" · "} + {r.meta} + + + + Avito + + {r.ppm} + {r.price} + + + Росреестр ↗ + +
+ ))} +
+ + {/* фактические сделки */} +
+
+
+
ПОХОЖИЕ КВАРТИРЫ · СДЕЛКИ
+
+ Фактические сделки по аналогичным квартирам +
+
+
+ Период: за 12 мес. +
+ проверенные данные · подтверждено Росреестром +
+
+ +
+ + + +
+ +
+ АДРЕС + ИСТОЧНИК + ₽/М² + ЦЕНА СДЕЛКИ + ДАТА +
+ + {dealRows.map((r: DealRow, i) => ( +
+ + {r.addr} + + {" · "} + {r.meta} + + + + Росреестр{" "} + + по улице + + + {r.ppm} + {r.price} + 01.01.26 +
+ ))} + +
+ Показано 10 фактических сделок +
+
+
+ ); +} diff --git a/frontend/src/components/trade-in/v2/TopNav.tsx b/frontend/src/components/trade-in/v2/TopNav.tsx new file mode 100644 index 00000000..c0b768df --- /dev/null +++ b/frontend/src/components/trade-in/v2/TopNav.tsx @@ -0,0 +1,407 @@ +"use client"; + +// Top navigation bar for the /trade-in/v2 "МЕРА Оценка" design port. +// 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/user come from fixtures, colours from +// tokens. + +import { useState } from "react"; +import type { CSSProperties } from "react"; + +import { tokens } from "./tokens"; +import { navLabels, user, version } from "./fixtures"; + +interface TopNavProps { + active: number; + onNavigate: (i: number) => void; +} + +const menuItemStyle: CSSProperties = { + display: "flex", + alignItems: "center", + gap: "10px", + padding: "9px 10px", + borderRadius: "6px", + cursor: "pointer", + fontSize: "12.5px", + color: tokens.body, + transition: "background .12s", +}; + +export default function TopNav({ active, onNavigate }: TopNavProps) { + const [userOpen, setUserOpen] = useState(false); + + return ( + <> + + +
+ {/* Logo + version */} +
+ + + + + + + + + + МЕРА + + + ОЦЕНКА НЕДВИЖИМОСТИ + + + +
+ {version} +
+
+ + {/* Nav tabs */} +
+ {navLabels.map((label, i) => ( +
onNavigate(i)} + className={`tnav-tab${active === i ? " tnav-tab--active" : ""}`} + style={{ position: "relative", whiteSpace: "nowrap" }} + > + {label} +
+ {i === 0 && ( +
+ )} +
+ ))} +
+ + {/* User menu */} +
+
setUserOpen((o) => !o)} + className="tnav-userbtn" + style={{ + display: "flex", + alignItems: "center", + gap: "10px", + cursor: "pointer", + padding: "5px 8px 5px 6px", + borderRadius: "9px", + transition: "background .15s", + }} + > +
+ {user.initials} +
+
+
+ {user.name} +
+
+ {user.org} +
+
+ +
+ + {userOpen && ( +
+
+
+ {user.initials} +
+
+
+ {user.name} +
+
+ {user.email} +
+
+
+ +
+ + + + + Профиль +
+ +
+ + + + + Мои отчёты{" "} + + {user.reports} + +
+ +
+ + + + + Настройки +
+ +
+ + + + + Помощь +
+ +
+ +
setUserOpen(false)} + className="tnav-logout" + style={{ ...menuItemStyle, color: tokens.danger }} + > + + + + Выйти +
+
+ )} +
+
+ + ); +} diff --git a/frontend/src/components/trade-in/v2/fixtures.ts b/frontend/src/components/trade-in/v2/fixtures.ts new file mode 100644 index 00000000..3f310437 --- /dev/null +++ b/frontend/src/components/trade-in/v2/fixtures.ts @@ -0,0 +1,665 @@ +// 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, + LocationFactors, + 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, + locationCoef: "0.87", + streetView: "Street View · май 2024", + compass: "СЕВЕРО-ЗАПАД", +}; + +// ---- 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 }, + { name: "N1.RU", 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", + }, + 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[] = [ + { + addr: "ул. Малышева, 30", + time: "12:45:02", + src: "3 / 7", + status: "свежий", + statusColor: tokens.success, + }, + { + addr: "ул. Куйбышева, 48", + time: "12:31:50", + src: "4 / 7", + status: "свежий", + statusColor: tokens.success, + }, + { + addr: "ул. Академика Ландау, 35", + time: "11:58:14", + src: "2 / 7", + status: "свежий", + statusColor: tokens.success, + }, + { + addr: "ул. 8 Марта, 120", + time: "09:12:40", + src: "3 / 7", + status: "устаревает", + statusColor: tokens.warn, + }, + { + addr: "пр. Ленина, 5", + time: "вчера 18:04", + src: "5 / 7", + status: "устарел", + statusColor: tokens.danger, + }, + { + addr: "ул. Белинского, 86", + time: "вчера 14:22", + src: "2 / 7", + status: "устарел", + statusColor: tokens.danger, + }, +]; + +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", + ], +}; + +// ---- КОЭФ. ЛОКАЦИИ DRAWER ------------------------------------------------- + +export const locationFactors: LocationFactors = { + coef: "0.87", + intro: + "Показывает, как адрес корректирует стоимость относительно медианы по Екатеринбургу. 1.00 — средний уровень. 0.87 означает, что локация снижает цену примерно на 13% из-за баланса факторов ниже.", + formula: { base: "11,29 млн", coef: "0.87", result: "9,82 млн ₽" }, + positives: [ + { label: "Центр города, пешая доступность ключевых точек", delta: "+0.06" }, + { label: "Транспортные узлы и остановки рядом", delta: "+0.05" }, + { label: "Набережная и парк в 10 минутах", delta: "+0.04" }, + { label: "Развитая торговая инфраструктура", delta: "+0.03" }, + ], + negatives: [ + { label: "Оживлённая магистраль, шумовая нагрузка", delta: "−0.07" }, + { label: "Дефицит парковочных мест", delta: "−0.05" }, + { label: "Возраст жилого фонда района (1985)", delta: "−0.04" }, + { label: "Износ инженерных сетей квартала", delta: "−0.02" }, + ], + footer: + "Источник геоданных: OpenStreetMap POI, транспортная доступность, шумовые и экологические слои. Коэффициент пересчитывается при смене адреса.", +}; + +// ---- 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"; diff --git a/frontend/src/components/trade-in/v2/tokens.ts b/frontend/src/components/trade-in/v2/tokens.ts new file mode 100644 index 00000000..0345e857 --- /dev/null +++ b/frontend/src/components/trade-in/v2/tokens.ts @@ -0,0 +1,115 @@ +// Design tokens for the /trade-in/v2 "МЕРА Оценка" HUD/blueprint port. +// Faithful colour palette extracted from the pixel design (МЕРА Оценка.dc.html). +// This file is the shared colour contract for every v2 component — import from +// here instead of hardcoding hex literals (one-off SVG stroke/opacity may stay +// inline). NOTE: this is an isolated design port, so it intentionally does NOT +// reuse the global GenDesign analytics tokens (different visual language). + +// Page background behind the 1536×1024 artboard. +export const pageBg = "#dde6ef"; + +export const tokens = { + // Brand / accent + accent: "#2e8bff", + accentDeep: "#0d6fd6", + accentLite: "#46a8ff", + onAccent: "#fff", // opaque white text/glyph on accent-coloured surfaces (avatar initials) + + // Ink / headings + ink: "#1c2c40", + ink2: "#17263a", + + // Body text + body: "#39506a", + body2: "#46586c", + + // Muted text (light → lighter) + muted: "#6f8195", + muted2: "#91a1b4", + muted3: "#9aaabb", + muted4: "#b3c0cf", + hint: "#8a9aac", // small helper / caption text + + // Borders & hairlines (strong → soft) + line: "#c2d3e3", + line2: "#c8d7e6", + line3: "#b8c8d8", // photo/card edge, chart axes + lineSoft: "#d3dfeb", + lineSoft2: "#e2e9f1", + lineSoft3: "#eef3f8", + lineSoft4: "#eaf0f6", // pale-blue hairline — house-sales row separator (design line 462) + lineGradient: "#cbd7e3", // decorative gradient line fade stops (footer hairline) + + // HUD corner brackets + bracket: "#9bbce0", + bracket2: "#8fb4dd", + + // Semantic + success: "#1baa6b", + successBorder: "#b6e0cc", // soft green border for success badges (В РАСЧЁТЕ) + warn: "#b58a3e", + gold: "#c79a3e", + danger: "#cd6868", + + // Info-soft panel (data-quality box) + infoSoftBg: "rgba(233,242,252,.5)", // translucent blue panel fill + infoSoftBorder: "#d3e0ec", // soft blue panel border + surfaceTint: "rgba(238,244,250,.5)", // translucent blue-tinted surface — KPI band + table footer (design lines 471, 487) + + // Data-viz tints (mini histograms / range bars / scatter) + barLo: "#cfddec", + barMid: "#a9cef5", + track: "#dbe6f1", // range track + donut track + scatterDeal: "#9aafc4", // grey "сделки" dots (price×time scatter charts) + dealDot: "#8fa3b8", // grey "сделки" marker — map dot + legend (design 186, 203); darker than scatterDeal + dotStroke: "#fff", // white outline ring around map marker dots (design 185-187) + + // Solid backgrounds + photoBg: "#e7eef6", // hero photo container fill behind building image + mapBg: "#e4ebf3", // map container fill behind blueprint SVG (design 156) + badgeTint: "#eaf1f8", // opaque pale-blue fill — source badge ("Яндекс") chip (design line 484) + + // Disabled / no-data source card (02 RESULT · inactive source tile, design lines 366-384) + disabledBorder: "#d8e2ec", // inactive source card edge + disabledValue: "#c2cdd9", // greyed-out "—" count value + disabledLabel: "#aab7c5", // greyed-out "нет данных" caption + + // Accent glow (range-marker dot halo, design lines 303/341) + accentGlow: "rgba(46,139,255,.5)", + + // Translucent white surfaces (glass cards / overlays) + surface: { + w25: "rgba(255,255,255,.25)", + w40: "rgba(255,255,255,.4)", + w50: "rgba(255,255,255,.5)", + w55: "rgba(255,255,255,.55)", + w60: "rgba(255,255,255,.6)", + w62: "rgba(255,255,255,.62)", + w65: "rgba(255,255,255,.65)", + w70: "rgba(255,255,255,.7)", + w72: "rgba(255,255,255,.72)", + w80: "rgba(255,255,255,.8)", + w85: "rgba(255,255,255,.85)", + w98: "rgba(255,255,255,.98)", + w99: "rgba(255,255,255,.99)", + }, + + // Font CSS variables (provided by app/trade-in/v2/layout.tsx via next/font) + font: { + sans: "var(--font-manrope), sans-serif", + mono: "var(--font-plex-mono), monospace", + }, + + // Artboard background (radial glows + blueprint grid + base gradient). + // Ported verbatim from the design root div (lines 23-29). + gradientBg: [ + "radial-gradient(620px 320px at 78% 16%, rgba(46,139,255,.07), transparent 60%)", + "radial-gradient(560px 360px at 24% 64%, rgba(46,139,255,.055), transparent 60%)", + "radial-gradient(480px 300px at 60% 92%, rgba(46,139,255,.04), transparent 60%)", + "repeating-linear-gradient(0deg, rgba(120,150,180,.045) 0 1px, transparent 1px 38px)", + "repeating-linear-gradient(90deg, rgba(120,150,180,.045) 0 1px, transparent 1px 38px)", + "linear-gradient(160deg,#f4f8fc 0%,#eaf1f8 55%,#e6eef7 100%)", + ].join(", "), + + pageBg, +}; diff --git a/frontend/src/components/trade-in/v2/types.ts b/frontend/src/components/trade-in/v2/types.ts new file mode 100644 index 00000000..61f0e5fc --- /dev/null +++ b/frontend/src/components/trade-in/v2/types.ts @@ -0,0 +1,284 @@ +// TypeScript shapes for the /trade-in/v2 design fixtures. +// All values are sample/markup data (see fixtures.ts) — no API types here. + +export interface Report { + id: string; + date: string; + validUntil: string; +} + +export interface ObjectInfo { + address: string; + city: string; + area: string; + rooms: string; + floor: string; + totalFloors: string; + year: string; + houseType: string; + repair: string; + balcony: boolean; + locationCoef: string; + streetView: string; + compass: string; +} + +// ---- 02 RESULT ------------------------------------------------------------ + +export interface ResultCard { + /** Header label, one entry per visual line. */ + title: string[]; + /** Big mono number, e.g. "11,29". */ + value: string; + /** Unit suffix, e.g. "млн ₽". */ + unit: string; + /** Low — high range line, e.g. "7,72 — 14,46 млн ₽". */ + range: string; + /** ₽/м² sub-line (may include a "· N сделки" tail). */ + ppm: string; + /** Mini-histogram column heights in % (cards 1 & 3). */ + bars?: number[]; + /** Donut delta value, e.g. "−11%" (card 2). */ + delta?: string; + /** Donut caption, e.g. "К РЫНКУ" (card 2). */ + deltaLabel?: string; + /** Nav index opened by the card's "Подробнее →" link. */ + nav: number; +} + +export interface ResultMeta { + sources: string; + confidence: string; + cv: string; +} + +export interface RangeMarker { + /** Filled segment offsets / dot position, as CSS % strings. */ + fillLeft: string; + fillRight: string; + dotLeft: string; +} + +export interface RangeBar { + /** Header label, one entry per visual line. */ + label: string[]; + median: string; + lo: string; + hi: string; + marker: RangeMarker; +} + +export interface Ranges { + ads: RangeBar; + deals: RangeBar; +} + +export interface ScatterPoint { + x: number; + y: number; + r: number; +} + +export interface AxisTickX { + label: string; + x: number; +} + +export interface AxisTickY { + label: string; + y: number; +} + +export interface ScatterMini { + deals: ScatterPoint[]; + analogs: ScatterPoint[]; + subject: ScatterPoint; + yTicks: AxisTickY[]; + xTicks: AxisTickX[]; +} + +export interface SourceCard { + name: string; + /** Lot count, e.g. "7" or "—" when no data. */ + count: string; + /** Count caption: "лотов" / "лот" / "нет данных". */ + label: string; + active: boolean; +} + +// ---- 03 SUMMARY ----------------------------------------------------------- + +export interface SummaryRow { + label: string; + value: string; + dot: "accent" | "muted"; + nav: number; +} + +export interface SummaryQuality { + sources: string; + confidence: string; + cv: string; +} + +export interface Summary { + rows: SummaryRow[]; + quality: SummaryQuality; +} + +// ---- OVERLAY: ПРОДАЖИ В ДОМЕ / ИСТОРИЯ ------------------------------------ + +export interface DkpRow { + area: string; + price: string; + ppm: string; + ask: string; + delta: string; + /** Colour for the delta (success / danger token value). */ + deltaColor: string; +} + +export interface HouseSaleRow { + lot: string; + priceFrom: string; + priceTo: string; + date: string; + exposure: string; +} + +export interface History { + houseSales: HouseSaleRow[]; + dkpHeader: { title: string; period: string; note: string }; + dkpKpi: { count: string; median: string; range: string }; +} + +// ---- OVERLAY: АНАЛОГИЧНЫЕ ОБЪЯВЛЕНИЯ -------------------------------------- + +export interface AdRow { + addr: string; + meta: string; + ppm: string; + price: string; +} + +export interface DealRow { + addr: string; + meta: string; + ppm: string; + price: string; +} + +export interface MarketAds { + kpi: { + count: string; + median: string; + ppm: string; + range: string; + cv: string; + }; + filters: string[]; +} + +export interface MarketDeals { + kpi: { count: string; median: string; delta: string; range: string }; +} + +// ---- OVERLAY: АНАЛИТИКА ДОМА ---------------------------------------------- + +export interface AnalyticsKpi { + label: string; + value: string; + unit?: string; + /** Optional value colour override (token value). */ + color?: string; + sub: string; +} + +export interface SellTimeTier { + tier: string; + days: string; + range: string; + count: string; + variant: "success" | "accent" | "gold" | "danger"; +} + +export interface PriceHistory { + note: string; + years: string[]; + /** SVG polyline points string (viewBox 0 0 900 220). */ + avito: string; + yandex: string; +} + +export interface ScatterDetail { + note: string; + deals: ScatterPoint[]; + analogs: ScatterPoint[]; + subject: ScatterPoint; + yTicks: AxisTickY[]; + xTicks: AxisTickX[]; +} + +export interface Analytics { + header: { ads: string; radius: string }; + kpis: AnalyticsKpi[]; + sellTime: SellTimeTier[]; + /** Sub-line above the sell-time tiers (carries the эталон ₽/м² number). */ + sellTimeNote: string; + priceHistory: PriceHistory; + scatterDetail: ScatterDetail; +} + +// ---- OVERLAY: ПРЕДЫДУЩИЕ ОЦЕНКИ (CACHE) ----------------------------------- + +export interface CacheRow { + addr: string; + time: string; + src: string; + status: string; + /** Status dot/text colour (success / warn / danger token value). */ + statusColor: string; +} + +export interface CacheKpi { + label: string; + value: string; + unit?: string; + sub: string; +} + +// ---- INPUTS / DROPDOWNS --------------------------------------------------- + +export interface DropdownOptions { + rooms: string[]; + houseType: string[]; + repair: string[]; + radius: string[]; + crm: string[]; +} + +// ---- КОЭФ. ЛОКАЦИИ DRAWER ------------------------------------------------- + +export interface LocationFactor { + label: string; + delta: string; +} + +export interface LocationFactors { + coef: string; + intro: string; + formula: { base: string; coef: string; result: string }; + positives: LocationFactor[]; + negatives: LocationFactor[]; + footer: string; +} + +// ---- USER / CHROME -------------------------------------------------------- + +export interface User { + name: string; + org: string; + email: string; + initials: string; + reports: number; +}