gendesign/frontend/src/components/analytics/RecommendShareSliders.tsx
2026-04-27 21:38:37 +03:00

127 lines
4 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";
interface Props {
bucketLabels: string[];
shares: number[]; // длина = bucketLabels.length, сумма ≈ 100
recommended: number[]; // оригинальные share_pct из API
onChange: (next: number[]) => void;
onReset: () => void;
}
/**
* Auto-rescale slider group. Когда пользователь двигает один слайдер до X%,
* остальные пересчитываются пропорционально к (100 - X)%, сохраняя их
* относительные доли. Если у других нет массы (sum=0) — распределяем поровну.
*/
export function RecommendShareSliders({
bucketLabels,
shares,
recommended,
onChange,
onReset,
}: Props) {
const handle = (idx: number, raw: number) => {
const v = Math.max(0, Math.min(100, raw));
const next = [...shares];
next[idx] = v;
const remaining = 100 - v;
const others = shares.filter((_, i) => i !== idx);
const otherSum = others.reduce((a, b) => a + b, 0);
if (otherSum <= 0) {
const each = remaining / others.length;
for (let i = 0; i < next.length; i++) {
if (i !== idx) next[i] = each;
}
} else {
for (let i = 0; i < next.length; i++) {
if (i !== idx) next[i] = (shares[i] * remaining) / otherSum;
}
}
// round to 1 decimal, fix sum-rounding drift on the changed slider
const rounded = next.map((x) => Math.round(x * 10) / 10);
const drift = 100 - rounded.reduce((a, b) => a + b, 0);
rounded[idx] = Math.round((rounded[idx] + drift) * 10) / 10;
onChange(rounded);
};
const dirty = shares.some(
(s, i) => Math.abs(s - (recommended[i] ?? 0)) > 0.05,
);
return (
<div>
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
marginBottom: 8,
}}
>
<span style={{ fontSize: 13, color: "#5b6066" }}>
Сумма: {shares.reduce((a, b) => a + b, 0).toFixed(1)} %
</span>
<button
onClick={onReset}
disabled={!dirty}
style={{
padding: "4px 10px",
fontSize: 12,
border: "1px solid #d1d5db",
borderRadius: 12,
background: dirty ? "#fff" : "#f6f7f9",
color: dirty ? "#1f2937" : "#9ca3af",
cursor: dirty ? "pointer" : "not-allowed",
}}
>
Сбросить к рекомендации
</button>
</div>
<div style={{ display: "grid", gap: 12 }}>
{bucketLabels.map((label, i) => {
const recValue = recommended[i] ?? 0;
const v = shares[i] ?? 0;
const delta = v - recValue;
return (
<div
key={label}
style={{
display: "grid",
gridTemplateColumns: "140px 1fr 90px",
gap: 12,
alignItems: "center",
}}
>
<span style={{ fontSize: 13, fontWeight: 500 }}>{label}</span>
<input
type="range"
min={0}
max={100}
step={0.5}
value={v}
onChange={(e) => handle(i, Number(e.target.value))}
style={{ width: "100%" }}
/>
<div style={{ textAlign: "right", fontSize: 13 }}>
<span style={{ fontWeight: 600 }}>{v.toFixed(1)}%</span>
{Math.abs(delta) > 0.1 ? (
<span
style={{
marginLeft: 6,
fontSize: 11,
color: delta > 0 ? "#0a7a3a" : "#b3261e",
}}
>
{delta > 0 ? "+" : ""}
{delta.toFixed(1)}
</span>
) : null}
</div>
</div>
);
})}
</div>
</div>
);
}