gendesign/frontend/src/components/site-finder/HorizonSelector.tsx
Light1YT 99c9a130bc
All checks were successful
CI / changes (push) Successful in 6s
CI / frontend-tests (push) Successful in 46s
CI / changes (pull_request) Successful in 6s
CI / frontend-tests (pull_request) Successful in 46s
CI / backend-tests (push) Successful in 6m35s
CI / backend-tests (pull_request) Successful in 6m30s
Deploy / changes (push) Successful in 5s
Deploy / build-backend (push) Successful in 2m14s
Deploy / build-frontend (push) Successful in 2m55s
Deploy / build-worker (push) Successful in 4m6s
Deploy / deploy (push) Successful in 1m40s
feat(forecast): расширить горизонт прогноза до 24 мес (ТЗ §12.1)
API отвергал ?horizon=24 (422), хотя ТЗ §12.1 называет 6/12/18/24, а движок
УЖЕ считает 24 на каждом ране: _DEFAULT_HORIZONS=(6,12,18,24) во всех 6 точках
стека (orchestrator/forecast-task/demand_supply_forecast/scenarios/
special_indices/report_assembler), PIPELINE_HORIZON_MONTHS=24.
_hidden_release_fraction клампит h/18→1.0 на 24 (без переполнения),
future_supply._horizon_weight расширяет окно чисто — скрытых ≤18 потолков нет.
Чистое расширение валидатора-enum, не новая математика.

Backend: _ALLOWED_FORECAST_HORIZONS → {6,12,18,24}, Query/docstring/error-msg.
Frontend: HorizonSelector HORIZONS=[6,12,18,24] (тип horizon=number, union не нужен;
прочие потребители data-driven через meta.horizons/forecasts_by_horizon).
Тесты: API принимает 24/отвергает 30; движок-тесты доказывают h=24 осмыслен
(поля посчитаны, demand(24)>demand(18), hidden созрел, индексы в диапазонах).

Closes #944 (Q1 горизонт 24)
2026-06-13 17:27:35 +05:00

85 lines
2.6 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;
}
export function HorizonSelector({ value, onChange, disabled = false }: Props) {
return (
<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}
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>
);
}