From 91b460b1570e2e901a7854af27057a7148808705 Mon Sep 17 00:00:00 2001 From: bot-backend Date: Thu, 6 Aug 2026 20:19:59 +0300 Subject: [PATCH] =?UTF-8?q?chore(frontend):=20=D1=84=D0=B8=D0=BA=D1=81?= =?UTF-8?q?=D1=82=D1=83=D1=80=D1=8B=20=D0=BC=D0=B0=D0=BA=D0=B5=D1=82=D0=B0?= =?UTF-8?q?=20=D0=BD=D0=B5=20=D0=BF=D0=BE=20=D1=83=D0=BC=D0=BE=D0=BB=D1=87?= =?UTF-8?q?=D0=B0=D0=BD=D0=B8=D1=8E=20+=20=D1=83=D0=B4=D0=B0=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20=D0=BE=D1=81=D0=B8=D1=80=D0=BE=D1=82=D0=B5?= =?UTF-8?q?=D0=B2=D1=88=D0=B8=D1=85=20=D0=BA=D0=BE=D0=BC=D0=BF=D0=BE=D0=BD?= =?UTF-8?q?=D0=B5=D0=BD=D1=82=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Волна 0 инвентаризации техдолга. МЕРА: у восьми компонентов витрины v2 проп data имел значение по умолчанию из fixtures.ts — sample-данных дизайн-макета. При сбое передачи данных компонент отрисовал бы выдуманные числа вместо пустого состояния; на платном экране оценки это неотличимо от правды. Проп стал обязательным. Цепная правка в SectionOverlay: его опциональные поля пробрасывались в теперь-обязательные пропы. Проверено: единственный потребитель — app/v2/page.tsx, всегда передаёт data явно. Отдельной preview-страницы у v2/fixtures.ts нет. Статический UI-конфиг из того же файла (лейблы, версия, опции) не тронут — он не про данные отчёта. Птица: удалены шесть осиротевших компонентов (ScoreCard, AnalysisBreadcrumb, AnalysisSidebar, MassingEconomics, UserAvatar, PticaPlaceholderPanel) — по каждому подтверждён ноль импортов по всему репозиторию, включая динамические. Подчищены два ссылающихся комментария. Проверка: tsc и eslint чисто на обоих фронтах, next build успешен, vitest Птицы 264 теста зелёные. --- .../src/components/site-finder/ScoreCard.tsx | 5 - .../analysis/AnalysisBreadcrumb.tsx | 82 ---- .../site-finder/analysis/AnalysisSidebar.tsx | 266 ------------ .../site-finder/analysis/MassingEconomics.tsx | 410 ------------------ .../site-finder/analysis/UserAvatar.tsx | 104 ----- .../ptica/PticaPlaceholderPanel.tsx | 22 - frontend/src/lib/concept-api.ts | 2 +- frontend/src/lib/nspdLinks.ts | 2 +- .../components/trade-in/v2/AnalyticsView.tsx | 14 +- .../src/components/trade-in/v2/CacheView.tsx | 14 +- .../src/components/trade-in/v2/Footer.tsx | 11 +- .../src/components/trade-in/v2/HeroBar.tsx | 11 +- .../components/trade-in/v2/HistoryView.tsx | 17 +- .../components/trade-in/v2/ObjectSummary.tsx | 16 +- .../components/trade-in/v2/ResultPanel.tsx | 23 +- .../components/trade-in/v2/SectionOverlay.tsx | 13 +- .../components/trade-in/v2/SourcesView.tsx | 27 +- 17 files changed, 66 insertions(+), 973 deletions(-) delete mode 100644 frontend/src/components/site-finder/ScoreCard.tsx delete mode 100644 frontend/src/components/site-finder/analysis/AnalysisBreadcrumb.tsx delete mode 100644 frontend/src/components/site-finder/analysis/AnalysisSidebar.tsx delete mode 100644 frontend/src/components/site-finder/analysis/MassingEconomics.tsx delete mode 100644 frontend/src/components/site-finder/analysis/UserAvatar.tsx delete mode 100644 frontend/src/components/site-finder/ptica/PticaPlaceholderPanel.tsx diff --git a/frontend/src/components/site-finder/ScoreCard.tsx b/frontend/src/components/site-finder/ScoreCard.tsx deleted file mode 100644 index 2229b1f1..00000000 --- a/frontend/src/components/site-finder/ScoreCard.tsx +++ /dev/null @@ -1,5 +0,0 @@ -// ScoreCard is superseded by the tabbed dashboard in page.tsx. -// Logic is now split into OverviewTab, EnvironmentTab, LandTab, MarketTab. -// File kept to avoid breaking any external imports; exports an empty stub. - -export {}; diff --git a/frontend/src/components/site-finder/analysis/AnalysisBreadcrumb.tsx b/frontend/src/components/site-finder/analysis/AnalysisBreadcrumb.tsx deleted file mode 100644 index b72e33c9..00000000 --- a/frontend/src/components/site-finder/analysis/AnalysisBreadcrumb.tsx +++ /dev/null @@ -1,82 +0,0 @@ -"use client"; - -import Link from "next/link"; -import { ChevronRight } from "lucide-react"; - -interface AnalysisBreadcrumbProps { - cadNum: string; -} - -export function AnalysisBreadcrumb({ cadNum }: AnalysisBreadcrumbProps) { - return ( - - ); -} diff --git a/frontend/src/components/site-finder/analysis/AnalysisSidebar.tsx b/frontend/src/components/site-finder/analysis/AnalysisSidebar.tsx deleted file mode 100644 index 487f5cd6..00000000 --- a/frontend/src/components/site-finder/analysis/AnalysisSidebar.tsx +++ /dev/null @@ -1,266 +0,0 @@ -"use client"; - -import React, { useEffect, useRef, useState } from "react"; -import { ExternalLink } from "lucide-react"; - -// ── Types ───────────────────────────────────────────────────────────────────── - -interface SubSection { - id: string; - label: string; -} - -interface NavSection { - id: string; - label: string; - sub?: SubSection[]; -} - -// ── Config ──────────────────────────────────────────────────────────────────── - -const NAV_SECTIONS: NavSection[] = [ - { id: "section-1", label: "1. Объект" }, - { id: "section-2", label: "2. Земля и риски" }, - { - id: "section-3", - label: "3. Рынок", - sub: [ - { id: "section-3-1", label: "3.1 Настройки выборки" }, - { id: "section-3-2", label: "3.2 Планировки" }, - { id: "section-3-3", label: "3.3 Остатки и скорость" }, - ], - }, - { id: "section-4", label: "4. Оценка" }, - { id: "section-5", label: "5. Атмосфера" }, - { - id: "section-6", - label: "6. Прогноз", - sub: [ - { id: "section-6-1", label: "6.1 Прогноз по горизонтам" }, - { id: "section-6-2", label: "6.2 Сценарии" }, - { id: "section-6-3", label: "6.3 Уверенность" }, - { id: "section-6-4", label: "6.4 Рекомендация по продукту" }, - { id: "section-6-5", label: "6.5 Прозрачность скоринга" }, - { id: "section-6-6", label: "6.6 Будущее предложение и конкуренты" }, - ], - }, -]; - -// All section IDs in scroll order (for IntersectionObserver) -const ALL_SECTION_IDS: string[] = NAV_SECTIONS.flatMap((s) => - s.sub ? [s.id, ...s.sub.map((sub) => sub.id)] : [s.id], -); - -// ── Component ───────────────────────────────────────────────────────────────── - -export function AnalysisSidebar() { - const [activeId, setActiveId] = useState(ALL_SECTION_IDS[0]); - const observerRef = useRef(null); - - // Scrollspy via IntersectionObserver - useEffect(() => { - const candidates = ALL_SECTION_IDS.map((id) => - document.getElementById(id), - ).filter((el): el is HTMLElement => el !== null); - - if (candidates.length === 0) return; - - // Track which sections are visible; pick topmost visible one - const visible = new Set(); - - observerRef.current = new IntersectionObserver( - (entries) => { - entries.forEach((entry) => { - if (entry.isIntersecting) { - visible.add(entry.target.id); - } else { - visible.delete(entry.target.id); - } - }); - - // Pick the topmost section that is currently visible - const next = ALL_SECTION_IDS.find((id) => visible.has(id)); - if (next) setActiveId(next); - }, - { - root: null, - // Trigger when section top enters top 60% of viewport - rootMargin: "-8px 0px -40% 0px", - threshold: 0, - }, - ); - - candidates.forEach((el) => observerRef.current!.observe(el)); - - return () => { - observerRef.current?.disconnect(); - }; - }, []); - - function handleAnchorClick( - e: React.MouseEvent, - targetId: string, - ) { - e.preventDefault(); - const el = document.getElementById(targetId); - if (el) { - el.scrollIntoView({ behavior: "smooth", block: "start" }); - } - setActiveId(targetId); - } - - return ( - - ); -} diff --git a/frontend/src/components/site-finder/analysis/MassingEconomics.tsx b/frontend/src/components/site-finder/analysis/MassingEconomics.tsx deleted file mode 100644 index 272c6239..00000000 --- a/frontend/src/components/site-finder/analysis/MassingEconomics.tsx +++ /dev/null @@ -1,410 +0,0 @@ -"use client"; - -/** - * MassingEconomics — LIVE financial KPI strip for «7. Концепция» (#1965 Stage 2b, - * epic #1953). - * - * Driven by the interactive 3D MassingScene: every time the user drags the - * этажность / секций sliders, Section7Concept maps the scene's `computeModel` - * result + the analysis context into a `MassingProgram` and hands it here via - * `program`. We POST it to `/api/v1/concepts/recompute` (debounced ~250 ms) and - * render the recomputed ТЭП + финмодель (NPV / IRR / выручка / себестоимость / - * прибыль / ROI). - * - * Robustness: - * • debounce — slider drags fire many programs; only the settled one is sent. - * • latest-wins — an in-flight request is superseded by a newer one via a - * monotonic request id; a stale response is dropped, never overwriting a - * fresher result (mutateAsync + id guard, no UI flicker from out-of-order). - * • last-good — on a failed recompute we keep the last successful values and - * show a subtle inline note rather than blanking the panel. - * • skeleton — a plain grey fade KPI grid while the FIRST recompute is in - * flight (no shimmer, per ui-conventions). - * - * Light-theme only (Section7 is light): the 3D viewport stays dark-canvas, but - * this strip uses the light KPI tokens via the shared KpiCard. - */ - -import { useEffect, useRef, useState } from "react"; -import { AlertTriangle } from "lucide-react"; - -import { KpiCard } from "@/components/analytics/KpiCard"; -import { Section } from "@/components/analytics/Section"; -import { - priceSourceCaption, - useRecomputeMassing, - type FinancialModel, - type MassingProgram, - type MassingRecomputeOutput, - type Teap, -} from "@/lib/concept-api"; - -const DEBOUNCE_MS = 250; - -// ── Formatters (ru microcopy, shared shape with ConceptVariantsResult) ───────── - -const nf = new Intl.NumberFormat("ru-RU", { maximumFractionDigits: 0 }); - -/** Compact ₽ for headline figures: "2.4 млрд ₽", "145 млн ₽". */ -function formatMoneyCompact(rub: number): string { - const abs = Math.abs(rub); - if (abs >= 1e9) return `${(rub / 1e9).toFixed(1)} млрд ₽`; - if (abs >= 1e6) return `${(rub / 1e6).toFixed(0)} млн ₽`; - return `${nf.format(Math.round(rub))} ₽`; -} - -function formatInt(n: number): string { - return nf.format(Math.round(n)); -} - -function formatPct(fraction: number): string { - return `${(fraction * 100).toFixed(1)}%`; -} - -function formatFar(far: number): string { - return far.toLocaleString("ru-RU", { - minimumFractionDigits: 2, - maximumFractionDigits: 2, - }); -} - -// ── KPI grid ─────────────────────────────────────────────────────────────────── - -interface KpiGridProps { - teap: Teap; - financial: FinancialModel; - /** Регламентная КСИТ-цель (max_far) — to flag the КСИТ over-cap. */ - farTarget: number; - /** True → факт-КСИТ превышает регламентный потолок (model.over). */ - ksitOver: boolean; - /** Dim the strip while a fresher recompute is in flight (last-good values). */ - stale: boolean; -} - -function KpiGrid({ - teap, - financial, - farTarget, - ksitOver, - stale, -}: KpiGridProps) { - const netPositive = - financial.net_profit_rub > 0 - ? true - : financial.net_profit_rub < 0 - ? false - : null; - - return ( -
- {/* ТЭП */} -
- - - - -
- - {/* Финмодель */} -
- - - - - 0 - ? true - : financial.npv_rub < 0 - ? false - : null, - }} - hint={`Сумма дисконтированных помесячных денежных потоков по графику стройки и продаж (ставка дисконта ${formatPct( - financial.discount_rate_used, - )} годовых).`} - /> - financial.discount_rate_used - ? true - : false, - }} - hint="Годовая внутренняя ставка доходности тех же денежных потоков (ставка, при которой NPV = 0)." - /> - -
-
- ); -} - -// ── Skeleton (grey fade, no shimmer — ui-conventions) ────────────────────────── - -function SkeletonGrid() { - const cells = Array.from({ length: 7 }); - return ( -