// Settings screen const { useState: useStateT, useEffect: useEffectT } = React; function SettingsScreen({ operators: ops, setOperators, showToast, currentOperator }) { const isAdmin = currentOperator?.role === "admin"; const defaultSection = isAdmin ? "operators" : "profile"; const [section, setSection] = useStateT(defaultSection); const [modalOpen, setModalOpen] = useStateT(false); const [editingOp, setEditingOp] = useStateT(null); const [confirmDelete, setConfirmDelete] = useStateT(null); const allSections = [ { id: "operators", label: "Операторы", icon: "operators", adminOnly: true }, { id: "profile", label: "Профиль", icon: "user", adminOnly: false }, { id: "schedule", label: "Расписание", icon: "clock", adminOnly: true }, { id: "ai", label: "ИИ-настройки", icon: "sparkles", adminOnly: true }, { id: "notifications", label: "Уведомления", icon: "bell", adminOnly: true }, { id: "kb", label: "База знаний", icon: "book", adminOnly: true }, ]; const sections = allSections.filter(s => !s.adminOnly || isAdmin); async function saveOperator(data) { try { if (editingOp) { const updated = await window.apiFetch("PUT", `/api/operators/${editingOp.id}`, data); setOperators((arr) => arr.map((o) => (o.id === editingOp.id ? { ...o, ...updated } : o))); showToast("Оператор обновлён"); } else { const created = await window.apiFetch("POST", "/api/operators", data); setOperators((arr) => [...arr, { ...created, closed: 0, avgTime: "—" }]); showToast("Оператор добавлен"); } } catch (e) { showToast("Ошибка сохранения"); } setModalOpen(false); } async function deleteOperator(op) { try { await window.apiFetch("DELETE", `/api/operators/${op.id}`); setOperators((arr) => arr.filter((o) => o.id !== op.id)); showToast("Оператор удалён"); } catch (e) { showToast("Ошибка удаления"); } setConfirmDelete(null); } return (
{section === "operators" && { setEditingOp(null); setModalOpen(true); }} onEdit={(op) => { setEditingOp(op); setModalOpen(true); }} onDelete={(op) => setConfirmDelete(op)} />} {section === "profile" && } {section === "schedule" && } {section === "ai" && } {section === "notifications" && } {section === "kb" && }
{modalOpen && setModalOpen(false)} onSave={saveOperator} />} {confirmDelete && (
setConfirmDelete(null)}>
e.stopPropagation()}>
Удалить оператора?
«{confirmDelete.name}» больше не сможет отвечать.
)}
); } function OperatorsSection({ operators, onAdd, onEdit, onDelete }) { return (

Операторы

{operators.length} операторов · {operators.filter((o) => o.online).length} онлайн
{operators.map((op) => ( ))} {operators.length === 0 && }
Имя Telegram Роль Статус Действия
{op.name}
{op.tg} {op.role === "admin" ? "Администратор" : "Агент"} {op.online ? "Онлайн" : "Офлайн"}
Нет операторов
); } function OperatorModal({ editing, onClose, onSave }) { const [name, setName] = useStateT(editing?.name || ""); const [tg, setTg] = useStateT(editing?.tg || "@"); const [role, setRole] = useStateT(editing?.role || "agent"); const [password, setPassword] = useStateT(""); const [pwErr, setPwErr] = useStateT(null); function submit(e) { e?.preventDefault(); if (!name.trim() || tg.length < 2) return; if (!editing && password && password.length < 6) { setPwErr("Минимум 6 символов"); return; } setPwErr(null); onSave({ name: name.trim(), tg: tg.trim(), role, password }); } return (
e.stopPropagation()}>
{editing ? "Редактировать" : "Добавить оператора"}
setName(e.target.value)} placeholder="Алексей Петров" className="w-full bg-[#0d0d12] border border-[#2a2a3a] rounded-lg px-3 py-2 text-sm text-[#f1f1f5] placeholder:text-[#6b7280] focus:outline-none focus:border-[#4F8EF7]/50" />
{ let v = e.target.value; if (!v.startsWith("@")) v = "@" + v.replace(/^@*/, ""); setTg(v); }} placeholder="@username" className="w-full bg-[#0d0d12] border border-[#2a2a3a] rounded-lg px-3 py-2 text-sm text-[#f1f1f5] placeholder:text-[#6b7280] focus:outline-none focus:border-[#4F8EF7]/50 font-mono" />
{[["agent","Агент","Отвечает на диалоги"],["admin","Администратор","Полный доступ"]].map(([val, label, desc]) => ( ))}
{!editing && (
setPassword(e.target.value)} placeholder="••••••••" className="w-full bg-[#0d0d12] border border-[#2a2a3a] rounded-lg px-3 py-2 text-sm text-[#f1f1f5] placeholder:text-[#6b7280] focus:outline-none focus:border-[#4F8EF7]/50" /> {pwErr &&
{pwErr}
}
)}
); } function ProfileSection({ showToast }) { const [current, setCurrent] = useStateT(""); const [newPw, setNewPw] = useStateT(""); const [newPw2, setNewPw2] = useStateT(""); const [loading, setLoading] = useStateT(false); const [err, setErr] = useStateT(null); async function submit(e) { e?.preventDefault(); if (newPw !== newPw2) { setErr("Пароли не совпадают"); return; } if (newPw.length < 6) { setErr("Новый пароль — минимум 6 символов"); return; } setLoading(true); setErr(null); try { await window.apiFetch("PUT", "/api/auth/password", { current_password: current, new_password: newPw, }); setCurrent(""); setNewPw(""); setNewPw2(""); showToast("Пароль изменён"); } catch { setErr("Неверный текущий пароль"); } finally { setLoading(false); } } return (

Профиль

Управление своим аккаунтом
Смена пароля
setCurrent(e.target.value)} className="w-full bg-[#0d0d12] border border-[#2a2a3a] rounded-lg px-3 py-2 text-sm text-[#f1f1f5] focus:outline-none focus:border-[#4F8EF7]/50" />
setNewPw(e.target.value)} className="w-full bg-[#0d0d12] border border-[#2a2a3a] rounded-lg px-3 py-2 text-sm text-[#f1f1f5] focus:outline-none focus:border-[#4F8EF7]/50" />
setNewPw2(e.target.value)} className="w-full bg-[#0d0d12] border border-[#2a2a3a] rounded-lg px-3 py-2 text-sm text-[#f1f1f5] focus:outline-none focus:border-[#4F8EF7]/50" />
{err && (
{err}
)}
); } function ScheduleSection({ showToast }) { const DAYS = [ {key:"mon",label:"Пн"},{key:"tue",label:"Вт"},{key:"wed",label:"Ср"}, {key:"thu",label:"Чт"},{key:"fri",label:"Пт"},{key:"sat",label:"Сб"},{key:"sun",label:"Вс"}, ]; const [schedule, setSchedule] = useStateT(null); useEffectT(() => { window.apiFetch("GET", "/api/settings/schedule").then(setSchedule).catch(() => {}); }, []); function setDay(key, field, value) { setSchedule((s) => ({ ...s, [key]: { ...s[key], [field]: value } })); } async function save() { try { await window.apiFetch("PUT", "/api/settings/schedule", { schedule }); showToast("Расписание сохранено"); } catch { showToast("Ошибка сохранения"); } } if (!schedule) return
Загрузка...
; return (

Расписание

Вне рабочего времени — автоответ пользователю
{DAYS.map(({ key, label }) => { const day = schedule[key] || { enabled: false, from: "09:00", to: "21:00" }; return (
{label}
setDay(key, "enabled", !day.enabled)} /> {day.enabled ? (
setDay(key, "from", e.target.value)} className="bg-[#0d0d12] border border-[#2a2a3a] rounded-lg px-2 py-1 text-sm text-[#f1f1f5] focus:outline-none focus:border-[#4F8EF7]/50" /> setDay(key, "to", e.target.value)} className="bg-[#0d0d12] border border-[#2a2a3a] rounded-lg px-2 py-1 text-sm text-[#f1f1f5] focus:outline-none focus:border-[#4F8EF7]/50" /> Рабочий
) : ( Выходной )}
); })}
); } function AISection({ showToast }) { const [settings, setSettings] = useStateT(null); useEffectT(() => { window.apiFetch("GET", "/api/settings/ai").then(setSettings).catch(() => {}); }, []); async function save() { try { await window.apiFetch("PUT", "/api/settings/ai", settings); showToast("Настройки ИИ сохранены"); } catch { showToast("Ошибка сохранения"); } } if (!settings) return
Загрузка...
; return (

ИИ-настройки

Сохраняется в БД и Redis — n8n подхватывает сразу
setSettings((s) => ({ ...s, auto_reply: !s.auto_reply }))} />} /> setSettings((s) => ({ ...s, handoff_enabled: !s.handoff_enabled }))} />} /> setSettings((s) => ({ ...s, classification_enabled: !s.classification_enabled }))} />} />
Температура модели
Чем выше — тем креативнее
{Number(settings.temperature).toFixed(2)}
setSettings((s) => ({ ...s, temperature: parseFloat(e.target.value) }))} className="w-full accent-[#4F8EF7]" />
Системный промпт
Инструкции для ИИ в начале каждого диалога