diff --git a/tradein-mvp/frontend/src/app/v2/page.tsx b/tradein-mvp/frontend/src/app/v2/page.tsx index b2fe36a3..2e0ecb4d 100644 --- a/tradein-mvp/frontend/src/app/v2/page.tsx +++ b/tradein-mvp/frontend/src/app/v2/page.tsx @@ -34,6 +34,7 @@ import { mapAnalytics, mapCache, mapHistory, + mapMarkers, mapObject, mapReport, mapResultPanel, @@ -57,6 +58,8 @@ import { useStreetDeals, } from "@/lib/trade-in-api"; import { useQuota } from "@/lib/useQuota"; +import { useMe } from "@/lib/useMe"; +import { logout } from "@/lib/logout"; // OUTER HUD FRAME + 4 corner brackets (design lines 31-37). Decorative, // non-interactive overlay drawn over the artboard gradient. The frame has @@ -409,6 +412,11 @@ export default function TradeInV2Page() { // via invalidateQueries in handleSubmit. const history = useEstimateHistory(); const quota = useQuota(); + // Real logged-in identity for the TopNav user menu (replaces the design + // fixture). /me returns {username, role, brand} — no display name / email, so + // name falls back to username, org to brand ?? role, email to "" (renders + // blank, never invented). undefined while loading → TopNav «Гость» fallback. + const me = useMe(); // Dashboard sub-hooks — each resolves independently; failure degrades its // section via the mappers (null input) rather than blanking the page. @@ -497,6 +505,24 @@ export default function TradeInV2Page() { // to the fixture default (undefined) for unwired/pre-load states. const reportsCount = quota.data?.used ?? history.data?.length; + // Real user for the TopNav menu. undefined until /me resolves → TopNav shows + // its neutral «Гость» fallback (never the old "Андрей Петров / Брусника"). + const topNavUser = useMemo(() => { + const scope = me.data; + if (!scope) return undefined; + return { + name: scope.username, + org: scope.brand ?? scope.role, + email: "", + initials: scope.username.slice(0, 2).toUpperCase(), + }; + }, [me.data]); + + // Analog price pins for the 01 map, projected from the real estimate. No + // estimate → mapMarkers(null) → [] → ParamsPanel renders only the subject pin + // (Finding #2: never the static fixture price markers). + const markers = useMemo(() => mapMarkers(estimate), [estimate]); + // Prefill for restore-by-id / re-estimate. Keyed remount applies it when the // estimate arrives async (useState reads initialValues only on mount). const initialValues = useMemo | undefined>( @@ -626,7 +652,13 @@ export default function TradeInV2Page() { flexDirection: "column", }} > - + {middleContent} {rightContent} diff --git a/tradein-mvp/frontend/src/components/trade-in/v2/HeroBar.tsx b/tradein-mvp/frontend/src/components/trade-in/v2/HeroBar.tsx index 08bc1fc1..55090e3a 100644 --- a/tradein-mvp/frontend/src/components/trade-in/v2/HeroBar.tsx +++ b/tradein-mvp/frontend/src/components/trade-in/v2/HeroBar.tsx @@ -1,6 +1,6 @@ "use client"; -import type { CSSProperties } from "react"; +import { useState, type CSSProperties } from "react"; import Image from "next/image"; import { API_BASE_URL } from "@/lib/api"; @@ -60,6 +60,9 @@ export default function HeroBar({ onOpenInfo, }: HeroBarProps) { const pdfHref = pdfDownloadHref(estimateId); + // Hide the building photo if the asset 404s/400s so the photoBg fill shows + // instead of a broken-image icon. + const [imgFailed, setImgFailed] = useState(false); const pdfBtnInner = ( <> @@ -202,6 +205,7 @@ export default function HeroBar({
- {data.houseSales.length} ЛОТА + {data.houseSales.length}{" "} + {pluralRu(data.houseSales.length, ["ЛОТ", "ЛОТА", "ЛОТОВ"])}
diff --git a/tradein-mvp/frontend/src/components/trade-in/v2/LocationDrawer.tsx b/tradein-mvp/frontend/src/components/trade-in/v2/LocationDrawer.tsx index 290ece6a..9c64e2be 100644 --- a/tradein-mvp/frontend/src/components/trade-in/v2/LocationDrawer.tsx +++ b/tradein-mvp/frontend/src/components/trade-in/v2/LocationDrawer.tsx @@ -1,73 +1,35 @@ "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). +// "ПОЯСНЕНИЕ К РАСЧЁТУ" right-side drawer for the /trade-in/v2 "МЕРА Оценка" +// design port. Opened from HeroBar (the "?" near "КОЭФ. ЛОКАЦИИ"). This used to +// render a FABRICATED location coefficient (0.87), a fake "base × coef = result" +// formula and invented POI factor lists with a false "Источник: OpenStreetMap" +// footer — none of which the backend produces (location-coef is deferred, +// backend #2045). It is now an HONEST methodology panel with STATIC, truthful +// documentation text only: how the estimate is built, and an explicit note that +// the location coefficient does not exist yet. Keeps the same drawer shell / +// slide animation / close button. Open/close is driven entirely by props; the +// only effect is an Escape-to-close keydown listener while open. +import { useEffect } from "react"; 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) { + // Escape-to-close (UI effect only — no HTTP). Active while the drawer is open. + useEffect(() => { + if (!open) return; + const onKey = (e: KeyboardEvent) => { + if (e.key === "Escape") onClose(); + }; + window.addEventListener("keydown", onKey); + return () => window.removeEventListener("keydown", onKey); + }, [open, onClose]); + return ( <>