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
После ролевой дискуссии (аналитик×комдир×собственник): - #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
109 lines
3.5 KiB
TypeScript
109 lines
3.5 KiB
TypeScript
"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",
|
||
}}
|
||
>
|
||
Смена горизонта пересчитывает прогноз заново (~10–30 сек)
|
||
</span>
|
||
</div>
|
||
);
|
||
}
|