From a34686ac380bc160b51262dd4a37d6ae84cc3cd4 Mon Sep 17 00:00:00 2001 From: lekss361 Date: Sat, 30 May 2026 22:51:29 +0300 Subject: [PATCH] =?UTF-8?q?feat(tradein):=20ui-preview/estimate=20?= =?UTF-8?q?=E2=80=94=20=D0=B2=D1=81=D0=B5=2017=20=D1=81=D0=B5=D0=BA=D1=86?= =?UTF-8?q?=D0=B8=D0=B9=20=D0=BD=D0=B0=20=D0=BE=D1=84=D1=84=D0=BB=D0=B0?= =?UTF-8?q?=D0=B9=D0=BD-=D1=84=D0=B8=D0=BA=D1=81=D1=82=D1=83=D1=80=D0=B5?= =?UTF-8?q?=20(#801)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Расширяет preview-маршрут до 1:1 с реальной страницей результата: - fixture.ts: добавлены FIXTURE_IMV, FIXTURE_HOUSES, FIXTURE_PLACEMENT, FIXTURE_ANALYTICS, FIXTURE_SELLTIME с правдоподобными данными (ЕКБ, 2-к ~9 млн) - page.tsx: все 17 секций в порядке page.tsx строки 228-276; QueryClient засевает placement-history / house-analytics / sell-time-sensitivity / cian-price-changes — ни одна секция не схлопывается в return null Секции: SourcesProgress, WhatIfPanel, HeroSummary, IMVBenchmark, CianValuationCard, HouseInfoCard, PlacementHistoryCard, HouseAnalyticsSection, PhotoUpload, ListingsCard, DealsCard, StreetDealsCard, MapCard, DistributionCard, PriceTrendCard, ExposureCard, OfferCard Closes #801 --- .../src/app/ui-preview/estimate/fixture.ts | 171 +++++++++++++++++- .../src/app/ui-preview/estimate/page.tsx | 96 +++++++++- 2 files changed, 263 insertions(+), 4 deletions(-) diff --git a/tradein-mvp/frontend/src/app/ui-preview/estimate/fixture.ts b/tradein-mvp/frontend/src/app/ui-preview/estimate/fixture.ts index 5ca782f3..d58bd1a9 100644 --- a/tradein-mvp/frontend/src/app/ui-preview/estimate/fixture.ts +++ b/tradein-mvp/frontend/src/app/ui-preview/estimate/fixture.ts @@ -5,7 +5,16 @@ // photo_url=null у всех лотов — намеренно, чтобы не тянуть внешние картинки // (чистый оффлайн-рендер для lighthouse/axe). -import type { AggregatedEstimate, AnalogLot, TradeInEstimateInput } from "@/types/trade-in"; +import type { + AggregatedEstimate, + AnalogLot, + HouseAnalyticsResponse, + HouseInfoForEstimate, + IMVBenchmarkResponse, + PlacementHistoryItem, + SellTimeSensitivityResponse, + TradeInEstimateInput, +} from "@/types/trade-in"; const analogs: AnalogLot[] = [ { @@ -187,3 +196,163 @@ export const FIXTURE_INPUT: TradeInEstimateInput = { repair_state: "good", has_balcony: true, }; + +// ── IMV Benchmark fixture ───────────────────────────────────────────────────── +// available:true обязательно — иначе IMVBenchmark возвращает null. +export const FIXTURE_IMV: IMVBenchmarkResponse = { + available: true, + cache_key: "ekb:preview-house:2k", + recommended_price: 10_200_000, + lower_price: 9_400_000, + higher_price: 11_000_000, + market_count: 7, + fetched_at: "2026-05-30T12:00:00Z", + our_median_price: 9_850_000, + diff_pct: -3.4, +}; + +// ── HouseInfo fixture ───────────────────────────────────────────────────────── +// houses.length > 0 обязательно — иначе HouseInfoCard возвращает null. +export const FIXTURE_HOUSES: HouseInfoForEstimate[] = [ + { + house_id: 42001, + ext_house_id: "ekb-repin-75", + address: "Екатеринбург, ул. Репина, 75", + short_address: "Репина, 75", + lat: 56.84, + lon: 60.57, + year_built: 2018, + total_floors: 11, + house_type: "monolith", + passenger_elevators: 2, + cargo_elevators: 1, + has_concierge: false, + closed_yard: true, + has_playground: true, + parking_type: "подземная", + developer_name: "Атомстройкомплекс", + rating: 4.3, + reviews_count: 28, + raw_characteristics: [], + }, +]; + +// ── PlacementHistory fixture ────────────────────────────────────────────────── +// items.length > 0 обязательно — иначе PlacementHistoryCard возвращает null. +export const FIXTURE_PLACEMENT: PlacementHistoryItem[] = [ + { + id: 1001, + source: "avito", + house_id: 42001, + ext_item_id: "avito-preview-1001", + title: "2к, 54 м², 7/16 эт.", + rooms: 2, + area_m2: 54, + floor: 7, + total_floors: 16, + start_price: 10_200_000, + start_price_date: "2025-11-01", + last_price: 9_700_000, + last_price_date: "2026-03-15", + removed_date: "2026-04-10", + exposure_days: 160, + notes: null, + }, + { + id: 1002, + source: "cian", + house_id: 42001, + ext_item_id: "cian-preview-1002", + title: "2к, 57 м², 4/10 эт.", + rooms: 2, + area_m2: 57, + floor: 4, + total_floors: 10, + start_price: 9_800_000, + start_price_date: "2025-09-10", + last_price: 9_500_000, + last_price_date: "2025-12-20", + removed_date: "2026-01-05", + exposure_days: 117, + notes: null, + }, + { + id: 1003, + source: "avito", + house_id: 42001, + ext_item_id: "avito-preview-1003", + title: "2к, 55 м², 5/11 эт.", + rooms: 2, + area_m2: 55, + floor: 5, + total_floors: 11, + start_price: 9_500_000, + start_price_date: "2025-06-01", + last_price: 9_100_000, + last_price_date: "2025-08-30", + removed_date: "2025-09-05", + exposure_days: 96, + notes: null, + }, +]; + +// ── HouseAnalytics fixture ──────────────────────────────────────────────────── +// kpi.total_lots > 0 обязательно — иначе HouseAnalyticsSection возвращает null. +// price_history >= 2 точки — для рендера PriceHistoryChart. +// recent_sold > 0 — для рендера RecentSoldList. +export const FIXTURE_ANALYTICS: HouseAnalyticsResponse = { + house_ids: [42001], + radius_m: 0, + kpi: { + total_lots: 38, + sold_count: 24, + sold_rate_pct: 63.2, + median_exposure_days: 112, + median_bargain_pct: 4.8, + }, + price_history: [ + { year: 2022, source: "avito_imv", median_price_per_m2: 155_000, n_lots: 6, median_price_rub: 8_525_000 }, + { year: 2023, source: "avito_imv", median_price_per_m2: 161_000, n_lots: 9, median_price_rub: 8_855_000 }, + { year: 2024, source: "avito_imv", median_price_per_m2: 170_000, n_lots: 12, median_price_rub: 9_350_000 }, + { year: 2025, source: "avito_imv", median_price_per_m2: 176_000, n_lots: 11, median_price_rub: 9_680_000 }, + ], + recent_sold: [ + { + id: 2001, + source: "avito_imv", + rooms: 2, + area_m2: 54, + floor: 7, + start_price: 10_200_000, + last_price: 9_700_000, + removed_date: "2026-04-10", + exposure_days: 160, + discount_pct: 4.9, + }, + { + id: 2002, + source: "avito_imv", + rooms: 2, + area_m2: 57, + floor: 4, + start_price: 9_800_000, + last_price: 9_500_000, + removed_date: "2026-01-05", + exposure_days: 117, + discount_pct: 3.1, + }, + ], +}; + +// ── SellTimeSensitivity fixture ─────────────────────────────────────────────── +export const FIXTURE_SELLTIME: SellTimeSensitivityResponse = { + house_ids: [42001], + radius_m: 0, + target_median_price_per_m2: 178_000, + buckets: [ + { price_premium_label: "cheap", price_premium_pct: -5, median_exposure_days: 38, p25_days: 21, p75_days: 55, n_lots: 8 }, + { price_premium_label: "median", price_premium_pct: 0, median_exposure_days: 112, p25_days: 60, p75_days: 160, n_lots: 14 }, + { price_premium_label: "plus5", price_premium_pct: 5, median_exposure_days: 185, p25_days: 90, p75_days: 240, n_lots: 10 }, + { price_premium_label: "plus10", price_premium_pct: 10, median_exposure_days: 270, p25_days: 150, p75_days: 360, n_lots: 6 }, + ], +}; diff --git a/tradein-mvp/frontend/src/app/ui-preview/estimate/page.tsx b/tradein-mvp/frontend/src/app/ui-preview/estimate/page.tsx index 2e21876f..5c858e1b 100644 --- a/tradein-mvp/frontend/src/app/ui-preview/estimate/page.tsx +++ b/tradein-mvp/frontend/src/app/ui-preview/estimate/page.tsx @@ -15,6 +15,25 @@ * Локально: echo NEXT_PUBLIC_ENABLE_PREVIEW=1 > .env.local && npm run dev * → http://localhost:3000/ui-preview/estimate * CI: собрать с флагом, поднять next start, навести axe/lighthouse. + * + * Секции (1:1 с page.tsx строки 228-276): + * 1. SourcesProgress — estimate prop + * 2. WhatIfPanel — baseEstimate + baseInput props + * 3. HeroSummary — estimate + input props + * 4. IMVBenchmark — benchmark prop (FIXTURE_IMV, available:true) + * 5. CianValuationCard — data prop + * 6. HouseInfoCard — houses prop (FIXTURE_HOUSES, length>0) + * 7. PlacementHistoryCard — estimateId; seed queryKey "placement-history" + * 8. HouseAnalyticsSection — estimateId; seed "house-analytics" + "sell-time-sensitivity" + * 9. PhotoUpload — estimateId (upload-UI; fetch photo может тихо упасть — ок) + * 10. ListingsCard — estimate + estimateId; seed "cian-price-changes" пустым + * 11. DealsCard — estimate prop + * 12. StreetDealsCard — estimate prop + * 13. MapCard — estimate prop (analogs имеют lat/lon → пины есть) + * 14. DistributionCard — estimate prop + * 15. PriceTrendCard — estimate prop + * 16. ExposureCard — estimate prop + * 17. OfferCard — estimate + brandSlug=null props */ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; @@ -27,14 +46,34 @@ import { DealsCard } from "@/components/trade-in/DealsCard"; import { DistributionCard } from "@/components/trade-in/DistributionCard"; import { ExposureCard } from "@/components/trade-in/ExposureCard"; import { HeroSummary } from "@/components/trade-in/HeroSummary"; +import { HouseAnalyticsSection } from "@/components/trade-in/HouseAnalyticsSection"; +import { HouseInfoCard } from "@/components/trade-in/HouseInfoCard"; +import { IMVBenchmark } from "@/components/trade-in/IMVBenchmark"; +import { ListingsCard } from "@/components/trade-in/ListingsCard"; +import { MapCard } from "@/components/trade-in/MapCard"; +import { OfferCard } from "@/components/trade-in/OfferCard"; +import { PhotoUpload } from "@/components/trade-in/PhotoUpload"; +import { PlacementHistoryCard } from "@/components/trade-in/PlacementHistoryCard"; import { PriceTrendCard } from "@/components/trade-in/PriceTrendCard"; +import { SourcesProgress } from "@/components/trade-in/SourcesProgress"; import { StreetDealsCard } from "@/components/trade-in/StreetDealsCard"; +import { WhatIfPanel } from "@/components/trade-in/WhatIfPanel"; import { ME_QUERY_KEY, type UserScope } from "@/lib/useMe"; -import { FIXTURE_ESTIMATE, FIXTURE_INPUT } from "./fixture"; +import { + FIXTURE_ANALYTICS, + FIXTURE_ESTIMATE, + FIXTURE_HOUSES, + FIXTURE_IMV, + FIXTURE_INPUT, + FIXTURE_PLACEMENT, + FIXTURE_SELLTIME, +} from "./fixture"; const ENABLED = process.env.NEXT_PUBLIC_ENABLE_PREVIEW === "1"; +const PREVIEW_ID = FIXTURE_ESTIMATE.estimate_id; + // Фейковый authorized-юзер: засеивается в QueryClient → useMe/useBrand резолвятся // оффлайн (никакого /me, /quota, /brand), generic-бренд (brand=null). const PREVIEW_USER: UserScope = { @@ -52,7 +91,15 @@ function PreviewContent() { queries: { retry: false, staleTime: Infinity, refetchOnWindowFocus: false }, }, }); + // /me — пропускает RouteGuard без сетевого запроса qc.setQueryData(ME_QUERY_KEY, PREVIEW_USER); + // Секция 7: PlacementHistoryCard — useEstimatePlacementHistory + qc.setQueryData(["trade-in", "estimate", PREVIEW_ID, "placement-history"], FIXTURE_PLACEMENT); + // Секция 8: HouseAnalyticsSection — useEstimateHouseAnalytics + useEstimateSellTimeSensitivity + qc.setQueryData(["trade-in", "estimate", PREVIEW_ID, "house-analytics"], FIXTURE_ANALYTICS); + qc.setQueryData(["trade-in", "estimate", PREVIEW_ID, "sell-time-sensitivity"], FIXTURE_SELLTIME); + // Секция 10: ListingsCard — useEstimateCianPriceChanges (пустой = graceful) + qc.setQueryData(["trade-in", "estimate", PREVIEW_ID, "cian-price-changes"], []); return qc; }); @@ -62,18 +109,61 @@ function PreviewContent() { (flex-column gap, --container ширина). Без них карточки рендерились узко/«мобильно». */}
+ {/* 1. SourcesProgress */} + + + {/* 2. WhatIfPanel */} + + + {/* 3. HeroSummary */} {}} isResubmitting={false} /> + + {/* 4. IMVBenchmark — передаём пропом напрямую (available:true → не null) */} + + + {/* 5. CianValuationCard */} - + + {/* 6. HouseInfoCard — передаём пропом напрямую (length>0 → не null) */} + + + {/* 7. PlacementHistoryCard — useEstimatePlacementHistory; queryKey засеян выше */} + + + {/* 8. HouseAnalyticsSection — useEstimateHouseAnalytics; queryKeys засеяны выше */} + + + {/* 9. PhotoUpload — рендерит upload-UI; fetch фото может тихо упасть — ок */} + + + {/* 10. ListingsCard — cian-price-changes засеян пустым массивом */} + + + {/* 11. DealsCard */} + + {/* 12. StreetDealsCard */} - + + {/* 13. MapCard — analogs имеют lat/lon → пины есть; Leaflet CDN → тайлы из сети (ок) */} + + + {/* 14. DistributionCard */} + + + {/* 15. PriceTrendCard */} + + {/* 16. ExposureCard */} + + + {/* 17. OfferCard */} +