From 0ea850c957c49d4adc9083100a0b35d1c375e41e Mon Sep 17 00:00:00 2001 From: bot-backend Date: Mon, 29 Jun 2026 02:12:04 +0300 Subject: [PATCH] =?UTF-8?q?feat(tradein/sale-share):=20=D1=82=D1=83=D0=BC?= =?UTF-8?q?=D0=B1=D0=BB=D0=B5=D1=80=20=C2=AB=D0=A1=D0=B5=D0=B9=D1=87=D0=B0?= =?UTF-8?q?=D1=81=20/=20=D0=97=D0=B0=2045=20=D0=B4=D0=BD=D0=B5=D0=B9=C2=BB?= =?UTF-8?q?=20+=20=D1=84=D0=B8=D0=BB=D1=8C=D1=82=D1=80=20=C2=AB=D0=BC?= =?UTF-8?q?=D0=B8=D0=BD.=20=D0=BE=D0=B1=D1=8A=D1=8F=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B9=C2=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #3: переключатель окна метрики (now/45д) — меняет показываемый % (sale_share_pct↔sale_share_pct_45d), счётчик (active_secondary↔listings_45d) и сортировку (share_desc↔share_45d_desc, active_desc↔count_45d_desc). 45д-данные уже были в API — фронт их не показывал (потому тумблер и не находился). #2: поле «Мин. объявлений в доме» — client-side фильтр по оконному счётчику. Frontend-only (бэк/API/deps не тронуты). typecheck clean. NB: маркеры карты пока по now-метрике (мелкий follow-up). --- .../frontend/src/app/sale-share/page.tsx | 41 +++++++++++++--- .../components/trade-in/SaleShareControls.tsx | 48 +++++++++++++++++++ .../src/components/trade-in/SaleShareList.tsx | 25 +++++++--- .../src/components/trade-in/trade-in.css | 43 +++++++++++++++++ tradein-mvp/frontend/src/types/sale-share.ts | 6 +++ 5 files changed, 150 insertions(+), 13 deletions(-) diff --git a/tradein-mvp/frontend/src/app/sale-share/page.tsx b/tradein-mvp/frontend/src/app/sale-share/page.tsx index 032f0cd3..df6eae51 100644 --- a/tradein-mvp/frontend/src/app/sale-share/page.tsx +++ b/tradein-mvp/frontend/src/app/sale-share/page.tsx @@ -49,6 +49,10 @@ export default function SaleSharePage() { const [yearMax, setYearMax] = useState(""); const [houseType, setHouseType] = useState(""); const [sort, setSort] = useState("share_desc"); + // Окно метрики: «сейчас» (активные сейчас) или «за 45 дней». + const [window, setWindow] = useState<"now" | "45d">("now"); + // Клиентский фильтр «мин. объявлений в доме» (см. filteredBuildings ниже). + const [minCount, setMinCount] = useState(0); // Выбранный дом (drawer) + наведённый дом (sync со списком/картой). const [selected, setSelected] = useState(null); @@ -62,6 +66,15 @@ export default function SaleSharePage() { const debYearMin = useDebouncedValue(yearMin, 400); const debYearMax = useDebouncedValue(yearMax, 400); + // Сорт, уходящий в API, зависит от окна: при «45 дней» share/active мапятся + // на 45-дневные метрики (остальные — цена/экспозиция — без изменений). + const apiSort: SaleShareSort = useMemo(() => { + if (window !== "45d") return sort; + if (sort === "share_desc") return "share_45d_desc"; + if (sort === "active_desc") return "count_45d_desc"; + return sort; + }, [sort, window]); + const query: SaleShareQuery = useMemo( () => ({ min_pct: debMinPct, @@ -71,19 +84,30 @@ export default function SaleSharePage() { year_min: toNumOrNull(debYearMin), year_max: toNumOrNull(debYearMax), house_type: houseType || null, - sort, + sort: apiSort, limit: 200, }), - [debMinPct, debCity, debPriceMin, debPriceMax, debYearMin, debYearMax, houseType, sort], + [debMinPct, debCity, debPriceMin, debPriceMax, debYearMin, debYearMax, houseType, apiSort], ); const summaryQ = useSaleShareSummary(); const buildingsQ = useSaleShareBuildings(query); const buildings = useMemo(() => buildingsQ.data ?? [], [buildingsQ.data]); + // Клиентский фильтр «мин. объявлений в доме» — намеренно НЕ серверный: + // не трогаем query-key/бэкенд, фильтруем уже загруженный список по окну. + // Счётчик null трактуем как 0; minCount === 0 → фильтр выключен. + const filteredBuildings = useMemo(() => { + if (minCount <= 0) return buildings; + return buildings.filter((b) => { + const count = (window === "45d" ? b.listings_45d : b.active_secondary) ?? 0; + return count >= minCount; + }); + }, [buildings, minCount, window]); + // Стабильные колбэки (маркеры карты замыкают их при отрисовке). - const buildingsRef = useRef(buildings); - buildingsRef.current = buildings; + const buildingsRef = useRef(filteredBuildings); + buildingsRef.current = filteredBuildings; const handleSelect = useCallback((houseId: number) => { setSelected(buildingsRef.current.find((b) => b.house_id === houseId) ?? null); }, []); @@ -164,6 +188,10 @@ export default function SaleSharePage() { houseTypeOptions={houseTypeOptions} sort={sort} onSort={setSort} + window={window} + onWindow={setWindow} + minCount={minCount} + onMinCount={setMinCount} />
@@ -181,7 +209,7 @@ export default function SaleSharePage() { diff --git a/tradein-mvp/frontend/src/components/trade-in/SaleShareControls.tsx b/tradein-mvp/frontend/src/components/trade-in/SaleShareControls.tsx index bb1f0932..fe0a1f5b 100644 --- a/tradein-mvp/frontend/src/components/trade-in/SaleShareControls.tsx +++ b/tradein-mvp/frontend/src/components/trade-in/SaleShareControls.tsx @@ -81,6 +81,10 @@ interface Props { houseTypeOptions: string[]; sort: SaleShareSort; onSort: (v: SaleShareSort) => void; + window: "now" | "45d"; + onWindow: (v: "now" | "45d") => void; + minCount: number; + onMinCount: (v: number) => void; } export function SaleShareControls({ @@ -102,6 +106,10 @@ export function SaleShareControls({ houseTypeOptions, sort, onSort, + window, + onWindow, + minCount, + onMinCount, }: Props) { const sliderMax = summary && summary.max_pct != null @@ -118,6 +126,30 @@ export function SaleShareControls({
+
+ + Окно метрики + +
+ + +
+
+
@@ -216,6 +248,22 @@ export function SaleShareControls({ ))}
+
+ + { + const n = Math.floor(Number(e.target.value)); + onMinCount(Number.isFinite(n) && n > 0 ? n : 0); + }} + placeholder="0" + /> +