/
keorlov
/
collsim
Обзор
Документация
Войти
/
keorlov
/
collsim
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
frontend/src/components/ParamForm.jsx
142 строки
10 KB
Konstantin Orlov
wip
14 июл 2026, 12:10
14 июл 2026, 12:10
c8f2d41
Код
Авторство
О чём код?
const fmt = new Intl.NumberFormat("ru-RU"); function Input({ label, value, onChange, type = "number", step, min, max, hint, disabled }) { return ( <label className="form-field"> <span className="form-label">{label}</span> <input type={type} value={value ?? ""} onChange={(e) => onChange(type === "number" ? Number(e.target.value) : e.target.value)} step={step} min={min} max={max} disabled={disabled} /> {hint && <span className="form-hint">{hint}</span>} </label> ); } function Select({ label, value, onChange, options, hint, disabled }) { return ( <label className="form-field"> <span className="form-label">{label}</span> <select value={value ?? ""} onChange={(e) => onChange(e.target.value)} disabled={disabled}> {options.map((o) => ( <option key={o.value} value={o.value}>{o.label}</option> ))} </select> {hint && <span className="form-hint">{hint}</span>} </label> ); } function Section({ title, children }) { return ( <div className="form-section"> <div className="form-section-title">{title}</div> <div className="form-grid">{children}</div> </div> ); } function ClientTypeSection({ label, data, onChange, disabled }) { return ( <Section title={label}> <Input label="Доля" value={data.share} onChange={(v) => onChange({ ...data, share: v })} step={0.05} min={0} max={1} hint={(data.share * 100).toFixed(0) + "%"} disabled={disabled} /> <Input label="Множитель hazard" value={data.hazard_multiplier} onChange={(v) => onChange({ ...data, hazard_multiplier: v })} step={0.1} min={0} max={5} disabled={disabled} /> </Section> ); } function ProductSection({ label, data, onChange, disabled }) { return ( <Section title={label}> <Input label="Доля" value={data.share} onChange={(v) => onChange({ ...data, share: v })} step={0.05} min={0} max={1} hint={(data.share * 100).toFixed(0) + "%"} disabled={disabled} /> <Input label="Средний баланс" value={data.avg_balance} onChange={(v) => onChange({ ...data, avg_balance: v })} step={10000} min={0} hint={fmt.format(data.avg_balance) + " ₽"} disabled={disabled} /> {data.term_days !== null && ( <Input label="Срок (дней)" value={data.term_days} onChange={(v) => onChange({ ...data, term_days: v })} step={1} min={1} hint={(data.term_days / 365).toFixed(1) + " лет"} disabled={disabled} /> )} <Input label="Множитель hazard" value={data.hazard_mult} onChange={(v) => onChange({ ...data, hazard_mult: v })} step={0.1} min={0} max={5} disabled={disabled} /> <Input label="Дневная ставка %" value={data.daily_interest_rate} onChange={(v) => onChange({ ...data, daily_interest_rate: v })} step={0.0001} min={0} max={1} hint={(data.daily_interest_rate * 100).toFixed(2) + "%"} disabled={disabled} /> <Input label="NIM годовая %" value={data.nim_annual_rate} onChange={(v) => onChange({ ...data, nim_annual_rate: v })} step={0.01} min={0} max={1} hint={(data.nim_annual_rate * 100).toFixed(0) + "%"} disabled={disabled} /> {data.daily_close_prob !== undefined && ( <Input label="Вероятность закрытия (день)" value={data.daily_close_prob} onChange={(v) => onChange({ ...data, daily_close_prob: v })} step={0.0001} min={0} max={1} hint={(data.daily_close_prob * 100).toFixed(2) + "%"} disabled={disabled} /> )} </Section> ); } export default function ParamForm({ config, onChange, title, disabled }) { const set = (path, value) => { const next = JSON.parse(JSON.stringify(config)); const keys = path.split("."); let obj = next; for (let i = 0; i < keys.length - 1; i++) { obj = obj[keys[i]]; } obj[keys[keys.length - 1]] = value; onChange(next); }; return ( <div className="param-form"> <h3 className="param-form-title">{title}</h3> <Section title="Симуляция"> <Select label="Оптимизация" value={config.simulation.optimization_mode} onChange={(v) => set("simulation.optimization_mode", v)} options={[{value: "greedy", label: "Greedy"}, {value: "uplift", label: "Uplift"}]} disabled={disabled} /> <Input label="Коэффициент Джини" value={config.simulation.gini} onChange={(v) => set("simulation.gini", v)} step={0.05} min={0} max={1} disabled={disabled} /> <Input label="Cure recovery rate" value={config.simulation.cure_recovery_rate} onChange={(v) => set("simulation.cure_recovery_rate", v)} step={0.01} min={0} max={1} hint={(config.simulation.cure_recovery_rate * 100).toFixed(0) + "%"} disabled={disabled} /> <Input label="Hard cure rate" value={config.simulation.hard_cure_rate} onChange={(v) => set("simulation.hard_cure_rate", v)} step={0.001} min={0} max={1} hint={(config.simulation.hard_cure_rate * 100).toFixed(1) + "%"} disabled={disabled} /> </Section> <Section title="Портфель"> <Input label="Начальные клиенты" value={config.portfolio.initial_clients} onChange={(v) => set("portfolio.initial_clients", v)} step={1000} min={100} hint={fmt.format(config.portfolio.initial_clients)} disabled={disabled} /> <Input label="Рост (в день)" value={config.portfolio.daily_growth_rate} onChange={(v) => set("portfolio.daily_growth_rate", v)} step={0.00001} min={0} max={1} hint={(config.portfolio.daily_growth_rate * 100).toFixed(3) + "%"} disabled={disabled} /> </Section> <Section title="Hazard"> <Input label="Базовая ставка (день)" value={config.hazard.base_daily_rate} onChange={(v) => set("hazard.base_daily_rate", v)} step={0.0001} min={0} max={1} hint={(config.hazard.base_daily_rate * 100).toFixed(3) + "%"} disabled={disabled} /> </Section> <ClientTypeSection label="Хронические" data={config.client_types.chronic} onChange={(v) => set("client_types.chronic", v)} disabled={disabled} /> <ClientTypeSection label="Забывчивые" data={config.client_types.forgetful} onChange={(v) => set("client_types.forgetful", v)} disabled={disabled} /> <ProductSection label="Ипотека" data={config.products.mortgage} onChange={(v) => set("products.mortgage", v)} disabled={disabled} /> <ProductSection label="Потребительский" data={config.products.potreb} onChange={(v) => set("products.potreb", v)} disabled={disabled} /> <ProductSection label="Карта" data={config.products.card} onChange={(v) => set("products.card", v)} disabled={disabled} /> <ProductSection label="Авто" data={config.products.auto} onChange={(v) => set("products.auto", v)} disabled={disabled} /> <Section title="Каналы"> <Input label="Waiting: стоимость" value={config.channels.waiting.cost} onChange={(v) => set("channels.waiting.cost", v)} step={1} min={0} hint="₽" disabled={disabled} /> <Input label="Waiting: p_base" value={config.channels.waiting.p_base} onChange={(v) => set("channels.waiting.p_base", v)} step={0.01} min={0} max={1} hint={(config.channels.waiting.p_base * 100).toFixed(0) + "%"} disabled={disabled} /> <Input label="SMS: стоимость" value={config.channels.sms.cost} onChange={(v) => set("channels.sms.cost", v)} step={1} min={0} hint="₽" disabled={disabled} /> <Input label="SMS: p_base" value={config.channels.sms.p_base} onChange={(v) => set("channels.sms.p_base", v)} step={0.01} min={0} max={1} hint={(config.channels.sms.p_base * 100).toFixed(0) + "%"} disabled={disabled} /> <Input label="Звонок: стоимость" value={config.channels.call.cost} onChange={(v) => set("channels.call.cost", v)} step={1} min={0} hint="₽" disabled={disabled} /> <Input label="Звонок: p_base" value={config.channels.call.p_base} onChange={(v) => set("channels.call.p_base", v)} step={0.01} min={0} max={1} hint={(config.channels.call.p_base * 100).toFixed(0) + "%"} disabled={disabled} /> </Section> <Section title="Бюджеты"> <Input label="SMS (день)" value={config.budgets.daily_sms} onChange={(v) => set("budgets.daily_sms", v)} step={1000} min={0} hint={fmt.format(config.budgets.daily_sms) + " ₽"} disabled={disabled} /> <Input label="Звонки (день)" value={config.budgets.daily_call} onChange={(v) => set("budgets.daily_call", v)} step={1000} min={0} hint={fmt.format(config.budgets.daily_call) + " ₽"} disabled={disabled} /> </Section> <Section title="Лимиты"> <Input label="Звонков в неделю" value={config.legal_limits.max_calls_per_week} onChange={(v) => set("legal_limits.max_calls_per_week", v)} step={1} min={0} disabled={disabled} /> <Input label="SMS в неделю" value={config.legal_limits.max_sms_per_week} onChange={(v) => set("legal_limits.max_sms_per_week", v)} step={1} min={0} disabled={disabled} /> </Section> <Section title="Hard Collection"> <Input label="Cession DPD" value={config.hard_collection.cession_dpd} onChange={(v) => set("hard_collection.cession_dpd", v)} step={1} min={1} hint="дней" disabled={disabled} /> <Input label="Cession Rate" value={config.hard_collection.cession_rate} onChange={(v) => set("hard_collection.cession_rate", v)} step={0.01} min={0} max={1} hint={(config.hard_collection.cession_rate * 100).toFixed(0) + "%"} disabled={disabled} /> </Section> <Section title="Начальный срез портфеля"> <Input label="Soft share" value={config.initial_state.soft_share} onChange={(v) => set("initial_state.soft_share", v)} step={0.001} min={0} max={1} hint={(config.initial_state.soft_share * 100).toFixed(1) + "%"} disabled={disabled} /> <Input label="Hard share" value={config.initial_state.hard_share} onChange={(v) => set("initial_state.hard_share", v)} step={0.001} min={0} max={1} hint={(config.initial_state.hard_share * 100).toFixed(1) + "%"} disabled={disabled} /> <Input label="Hard DPD max" value={config.initial_state.hard_dpd_max} onChange={(v) => set("initial_state.hard_dpd_max", v)} step={1} min={90} hint="дней" disabled={disabled} /> </Section> </div> ); }