gendesign/frontend/src/components/site-finder/HorizonSelector.tsx
Light1YT 8cd0620543
All checks were successful
CI / changes (pull_request) Successful in 8s
CI / changes (push) Successful in 9s
CI / frontend-tests (push) Successful in 1m43s
CI / frontend-tests (pull_request) Successful in 1m59s
CI / openapi-codegen-check (push) Successful in 2m43s
CI / openapi-codegen-check (pull_request) Successful in 1m55s
CI / backend-tests (push) Successful in 9m28s
CI / backend-tests (pull_request) Successful in 9m28s
feat(site-finder): продуктовые решения по фидбеку analyze (#1741 #1742 #1743 #1744 #1745)
После ролевой дискуссии (аналитик×комдир×собственник):
- #1741 секции в 2 группы «Участок»→«Стройка и рынок» (Section1→4→2→3→5→6), gate-баннер вынесен НАД группой (убран дубль из Section4Estimate), nav-группы
- #1742 объяснение выбора класса: фраза-причина + мини-таблица 3 классов по deficit_index («+1 острый дефицит, −1 затоварка»). POI «обеспеченность»/market-overstock relabel — N/A (в UI не выводятся)
- #1743 «Остатки и скорость»→«Как продаётся рынок рядом» + headline-вердикт + «Темп продаж»/«Будущее предложение конкурентов (24 мес)» (убран англицизм Velocity-score/Pipeline) + интерпретированные единицы
- #1744 горизонт: caption+tooltip «пересчёт ~10-30с» + статус «Пересчитываем…»; колонки прогноза «…, квартир» + caveat advisory
- #1745 фаза1: таблица «Прогноз по всем форматам» (студия/1-5к × индекс дефицита/прогноз спроса/сигнал); backend пробросил per-format projected_demand_units + signal. Слайдеры/рубли — фаза2 после бэктеста #951

Verify: tsc 0 ошибок; py_compile; what_to_build/recommendation/report/exporter тесты pass.

Closes #1741
Closes #1742
Closes #1743
Closes #1744
Closes #1745
2026-06-18 12:18:04 +05:00

109 lines
3.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
/**
* HorizonSelector — segmented control «Горизонт прогноза» (958-B1 / #996).
*
* Controlled segmented control over the forecast horizons accepted by
* POST /api/v1/parcels/{cad}/analyze?horizon= (6 / 12 / 18 / 24; default 12).
* Drives useParcelAnalyzeQuery on the Site Finder analysis screen.
*
* A11y: role="radiogroup" wrapper + role="radio"/aria-checked per <button>
* segment (real buttons → native keyboard focus; visible focus ring kept).
*/
const HORIZONS = [6, 12, 18, 24] as const;
interface Props {
/** Currently selected horizon (one of 6 / 12 / 18 / 24). */
value: number;
/** Called with the newly selected horizon. */
onChange: (horizon: number) => void;
/** Disable the control while a re-analyze is in flight (prevents spamming). */
disabled?: boolean;
}
// Подсказка на сегментах — поясняет, что даёт ближний / дальний горизонт.
const HORIZON_TOOLTIP =
"6 мес — ближний спрос, точнее; 24 мес — стратегия, шире интервал";
export function HorizonSelector({ value, onChange, disabled = false }: Props) {
return (
<div
style={{
display: "flex",
flexDirection: "column",
alignItems: "flex-end",
gap: 6,
}}
>
<div style={{ display: "flex", alignItems: "center", gap: 12 }}>
<span
style={{
fontSize: 12,
fontWeight: 500,
letterSpacing: "0.04em",
textTransform: "uppercase",
color: "var(--fg-secondary)",
whiteSpace: "nowrap",
}}
>
Горизонт прогноза
</span>
<div
role="radiogroup"
aria-label="Горизонт прогноза"
style={{
display: "inline-flex",
borderRadius: 8,
border: "1px solid var(--border-card)",
background: "var(--bg-card)",
padding: 2,
gap: 2,
opacity: disabled ? 0.6 : 1,
}}
>
{HORIZONS.map((h) => {
const selected = h === value;
return (
<button
key={h}
type="button"
role="radio"
aria-checked={selected}
disabled={disabled}
title={HORIZON_TOOLTIP}
onClick={() => onChange(h)}
style={{
appearance: "none",
cursor: disabled ? "not-allowed" : "pointer",
padding: "6px 16px",
borderRadius: 6,
border: "none",
background: selected ? "var(--accent)" : "transparent",
color: selected ? "var(--fg-on-dark)" : "var(--fg-secondary)",
fontSize: 13,
fontWeight: selected ? 600 : 500,
fontVariantNumeric: "tabular-nums",
whiteSpace: "nowrap",
transition: "background 0.12s, color 0.12s",
}}
>
{h} мес
</button>
);
})}
</div>
</div>
<span
style={{
fontSize: 12,
lineHeight: "16px",
color: "var(--fg-tertiary)",
textAlign: "right",
}}
>
Смена горизонта пересчитывает прогноз заново (~1030 сек)
</span>
</div>
);
}