import React,{useEffect,useState,useRef} from 'react';
import {createRoot} from 'react-dom/client';
import {motion,useReducedMotion} from 'framer-motion';
import {LayoutDashboard,Users,Shield,Coins,Send,History,Settings,LogOut,Search,ArrowUpRight,ArrowRight,Check,Activity,Radio,RefreshCw,LockKeyhole,ChevronLeft,ChevronRight,Terminal,Download,AlertCircle,X} from 'lucide-react';
import '@fontsource/manrope/400.css';
import '@fontsource/manrope/500.css';
import '@fontsource/manrope/600.css';
import '@fontsource/manrope/700.css';
import '@fontsource/manrope/800.css';
import './style.css';

let csrf='';
const fmt=n=>n==null?'—':Number(n).toLocaleString('ru-RU');
const date=n=>new Date(n*1000).toLocaleString('ru-RU',{dateStyle:'short',timeStyle:'short'});
const roles={tech:'Технический администратор',owner:'Владелец сервера',moderator:'Модератор',support:'Саппорт'};
const labels={members:'Просмотр участников',timeout:'Выдача таймаута',untimeout:'Снятие таймаута',ban:'Роль бана',unban:'Разбан и восстановление ролей',balance:'Просмотр баланса',coins:'Изменение монет',send_channel:'Сообщения в каналы',send_dm:'Личные сообщения',audit:'Журнал аудита',accounts:'Управление аккаунтами',login:'Вход',account:'Изменение аккаунта',password:'Смена пароля',discord:'Действие в Discord'};
async function api(path,body){
 const response=await fetch('/api/dashboard/'+path,{method:body?'POST':'GET',credentials:'same-origin',headers:body?{'Content-Type':'application/json','X-CSRF-Token':csrf}:{},body:body?JSON.stringify(body):undefined,signal:AbortSignal.timeout(30000)});
 let data;try{data=await response.json()}catch{throw new Error('Панель не получила ответ сервера.')}
 if(!response.ok){const e=new Error(data.error||'Не удалось выполнить запрос.');e.status=response.status;throw e}return data;
}
function IconButton({icon:Icon,label,...props}){return <button className="icon-button" aria-label={label} title={label} {...props}><Icon size={20}/></button>}
function Empty({children}){return <p className="empty">{children}</p>}
function Field({label,children}){return <label className="field"><span>{label}</span>{children}</label>}
function Pager({offset,total,size,onChange}){return <div className="pager"><span>{total?`${offset+1}–${Math.min(offset+size,total)} из ${fmt(total)}`:'Нет записей'}</span><IconButton icon={ChevronLeft} label="Предыдущая страница" disabled={!offset} onClick={()=>onChange(Math.max(0,offset-size))}/><IconButton icon={ChevronRight} label="Следующая страница" disabled={offset+size>=total} onClick={()=>onChange(offset+size)}/></div>}
function StatCard({title,value,icon:Icon,change}){const reduced=useReducedMotion();return <motion.div whileHover={reduced?{}:{y:-4}} className="stat-card"><div className="stat-top"><Icon size={21}/><span>{change}</span></div><strong>{value}</strong><span>{title}</span></motion.div>}
function AuditRows({items,compact=false}){if(!items)return <Empty>Загружаем события…</Empty>;return items.length?<div className={compact?'feed':'audit-table'}>{items.map(r=><div className="audit-row" key={r.id}><span className={'event-icon '+r.status}>{r.status==='success'?<Check size={16}/>:<AlertCircle size={16}/>}</span><div><strong>{labels[r.action]||r.action}</strong><small>{r.actor} {r.target&&`→ ${r.target}`}</small>{!compact&&<><p>{r.reason}</p><details><summary>Подробности</summary><pre>{r.detail}</pre></details></>}</div><div className="event-time"><time>{date(r.created)}</time><span>{({success:'Выполнено',error:'Ошибка',pending:'Ожидание',denied:'Отказ'})[r.status]||r.status}</span></div></div>)}</div>:<Empty>Пока нет событий.</Empty>}
function App(){
 const [user,setUser]=useState(null),[boot,setBoot]=useState(true),[tab,setTab]=useState('overview'),[notice,setNotice]=useState(null),[busy,setBusy]=useState(false),[version,setVersion]=useState(0),[confirm,setConfirm]=useState(null),[retry,setRetry]=useState(null);
 const dialogRef=useRef(null);useEffect(()=>{if(confirm)dialogRef.current?.showModal()},[confirm]);
 const can=p=>user?.permissions.includes(p);
 const refresh=()=>setVersion(v=>v+1);
 useEffect(()=>{api('me').then(d=>{csrf=d.csrf;setUser(d.user)}).catch(e=>{if(e.status!==401)setNotice({error:true,text:e.message})}).finally(()=>setBoot(false))},[]);
 async function login(e){e.preventDefault();setBusy(true);const data=Object.fromEntries(new FormData(e.currentTarget));try{const d=await api('login',data);csrf=d.csrf;setUser(d.user);setNotice(null)}catch(e){setNotice({error:true,text:e.message})}finally{setBusy(false)}}
 async function logout(){try{await api('logout',{});setUser(null);csrf=''}catch(e){setNotice({error:true,text:e.message})}}
 async function perform(body){if(busy)return;setBusy(true);const signature=JSON.stringify(body),key=retry?.signature===signature?retry.key:crypto.randomUUID();setRetry({signature,key});try{const r=await api('action',{...body,key});setRetry(null);setConfirm(null);setNotice({text:r.message||'Готово'});refresh();if(body.action==='password'){setUser(null);csrf=''}}catch(e){setNotice({error:true,text:e.message})}finally{setBusy(false)}}
 function request(body,description){setNotice(null);setConfirm({body,description})}
 const shared={can,request,version,fail:e=>setNotice({error:true,text:e.message})};
 const navigation=[['overview','Обзор',LayoutDashboard,true],['members','Участники',Users,can('members')],['messages','Сообщения',Send,can('send_channel')||can('send_dm')],['audit','Аудит',History,can('audit')],['accounts','Доступ',Shield,can('accounts')],['console','Консоль',Terminal,can('timeout')||can('coins')],['settings','Настройки',Settings,true]];
 if(boot)return <div className="loading-page"><Activity/><p>Подключаем панель…</p></div>;
 return <>{!user?<main className="login-layout"><div className="login-intro"><div className="wordmark"><Mark/> БАЗА МИРО</div><h1>Твой сервер.<br/><span>Под контролем.</span></h1><p>Участники, модерация и экономика —<br/>в одном рабочем пространстве.</p><div className="login-points"><span><Shield size={18}/> Раздельные права</span><span><History size={18}/> История действий</span></div></div><form className="login-card panel" onSubmit={login}><LockKeyhole size={28}/><h2>Вход в управление</h2><p className="muted">Для команды сервера База Миро</p><Field label="Логин"><input name="login" autoComplete="username" required maxLength={32}/></Field><Field label="Пароль"><input name="password" type="password" autoComplete="current-password" required maxLength={256}/></Field><button className="primary" disabled={busy}>{busy?'Проверяем…':'Войти в панель'}<ArrowRight size={18}/></button><small>Нет доступа? Обратись к техническому администратору.</small></form></main>:<div className="shell"><aside className="rail"><a className="mark" href="#overview" aria-label="Главная" onClick={()=>setTab('overview')}><Mark/></a><nav aria-label="Разделы панели">{navigation.filter(n=>n[3]).map(([key,title,Icon])=><button key={key} className={tab===key?'selected':''} aria-label={title} title={title} onClick={()=>setTab(key)}><Icon size={22}/><span>{title}</span></button>)}</nav><button className="rail-exit" onClick={logout} aria-label="Выйти" title="Выйти"><LogOut size={21}/></button></aside><main className="workspace"><header className="topbar"><div className="breadcrumb">База Миро <span>/</span> <strong>{navigation.find(n=>n[0]===tab)?.[1]}</strong></div><div className="profile"><span className="avatar">{user.login.slice(0,1).toUpperCase()}</span><div><strong>{user.login}</strong><small>{roles[user.role]}</small></div></div></header><div className="page-heading"><div><h1>{tab==='overview'?`Добро пожаловать, ${user.login}`:navigation.find(n=>n[0]===tab)?.[1]}</h1><p>{tab==='overview'?'Всё важное о сервере — здесь и сейчас.':'Рабочее пространство команды Базы Миро'}</p></div><button className="subtle" onClick={refresh}><RefreshCw size={16}/>Обновить</button></div>
 <div className="tab-content" key={tab}>
 {tab==='overview'&&<Overview {...shared} go={setTab}/>}
 {tab==='members'&&<Members {...shared}/>}
 {tab==='messages'&&<Messages {...shared}/>}
 {tab==='audit'&&<Audit {...shared}/>}
 {tab==='accounts'&&<Accounts {...shared} user={user}/>}
 {tab==='console'&&<Console {...shared}/>}
 {tab==='settings'&&<SettingsPage request={request} user={user}/>}
 </div>
 <footer><span>База Миро · Панель управления</span><span>Один сервер. Одна команда.</span></footer></main></div>}
 {notice&&<div role={notice.error?'alert':'status'} className={'toast '+(notice.error?'error':'')}><span>{notice.error?<AlertCircle size={19}/>:<Check size={19}/>}</span><p>{notice.text}</p><IconButton icon={X} label="Закрыть уведомление" onClick={()=>setNotice(null)}/></div>}
 {confirm&&<dialog ref={dialogRef} onCancel={e=>{if(busy)e.preventDefault();else setConfirm(null)}} className="confirm-dialog" aria-labelledby="confirm-title"><form onSubmit={e=>{e.preventDefault();perform(confirm.body)}}><Shield size={28}/><h2 id="confirm-title">Подтвердить действие</h2><p>{confirm.description}</p>{confirm.body.text&&<pre className="message-preview">{confirm.body.text}</pre>}<p className="muted">{confirm.body.reason}</p><div className="button-row"><button type="button" disabled={busy} onClick={()=>setConfirm(null)}>Отмена</button><button className="primary" disabled={busy}>{busy?'Выполняем…':'Подтвердить'}</button></div></form></dialog>}
 </>;
}
function Mark(){return <svg width="32" height="32" viewBox="0 0 40 40" aria-hidden="true"><path fill="currentColor" d="M4 8 14 15 20 3 26 15 36 8 32 35 20 27 8 35Z"/></svg>}
function LoadError({data}){return <section className="panel" role="alert"><h2>Не удалось загрузить данные</h2><p>{data.error}</p><button onClick={data.retry}>Повторить загрузку</button></section>}
function useData(path,version,fail){const [data,setData]=useState(null),[attempt,setAttempt]=useState(0);useEffect(()=>{let active=true;setData(null);api(path).then(d=>{if(active)setData(d)}).catch(e=>{if(active){setData({error:e.message,retry:()=>setAttempt(v=>v+1)});fail(e)}});return()=>{active=false}},[path,version,attempt]);return data}
function Overview({go,can,version,fail}){
 const d=useData('overview',version,fail);if(d?.error)return <LoadError data={d}/>;
 return <><div className="overview-grid"><section className="hero-panel"><div className="hero-top"><span className="status-pill"><i/>{d?.online?'Бот на связи':d?'Бот недоступен':'Проверяем связь'}</span><span>Центр управления</span></div><h2>Хороший сервер<br/>начинается с <em>команды.</em></h2><p>{d?d.guild.name:'База Миро'} — модерируй спокойно,<br/>держи экономику под контролем.</p><div className="hero-bottom"><button onClick={()=>go(can('members')?'members':'settings')}>Открыть участников <ArrowUpRight size={19}/></button>{can('audit')&&<button className="hero-ghost" onClick={()=>go('audit')}>Журнал действий</button>}</div><div className="hero-symbol" aria-hidden="true"><Shield strokeWidth={.8}/></div></section><section className="panel quick-panel"><div className="section-head"><h2>Рабочая смена</h2><Radio size={19}/></div><p className="today">{new Date().toLocaleDateString('ru-RU',{day:'numeric',month:'long'})}</p><p className="muted">Быстрый доступ без лишних шагов</p><div className="quick-links">{can('timeout')&&<button onClick={()=>go('members')}><Shield/><span>Модерация<small>Найти и выбрать участника</small></span><ArrowUpRight/></button>}{can('balance')&&<button onClick={()=>go('members')}><Coins/><span>Баланс и казино<small>Монеты и история операций</small></span><ArrowUpRight/></button>}{can('send_channel')&&<button onClick={()=>go('messages')}><Send/><span>Написать от бота<small>Каналы и личные сообщения</small></span><ArrowUpRight/></button>}</div></section></div><div className="stats-grid"><StatCard title="Участников сервера" value={fmt(d?.members)} icon={Users} change="Discord"/><StatCard title="Монет в обороте" value={fmt(d?.coins)} icon={Coins} change="Общий баланс"/><StatCard title="Действий за 24 часа" value={fmt(d?.actions_today)} icon={Shield} change="Панель"/><StatCard title="Задержка бота" value={d?`${fmt(d.latency)} мс`:'—'} icon={Activity} change={d?.online?'Подключён':'Проверка'}/></div>{can('audit')&&<section className="panel recent"><div className="section-head"><h2>Последние события</h2><button className="text-button" onClick={()=>go('audit')}>Весь журнал <ArrowUpRight size={16}/></button></div><AuditRows items={d?.recent} compact/></section>}</>;
}
function Members({can,request,version,fail}){
 const [q,setQ]=useState(''),[search,setSearch]=useState(''),[offset,setOffset]=useState(0),[selected,setSelected]=useState(null);
 useEffect(()=>{const t=setTimeout(()=>{setSearch(q);setOffset(0)},250);return()=>clearTimeout(t)},[q]);
 const d=useData('members?'+new URLSearchParams({q:search,offset}),version,fail);
 const detail=useData(selected?'member?id='+selected:'overview',version,fail);if(d?.error)return <LoadError data={d}/>;
 return <div className="member-layout"><section className="panel"><div className="search"><Search size={19}/><input aria-label="Поиск участников" placeholder="Ник, имя или Discord ID" value={q} onChange={e=>setQ(e.target.value)}/></div><div className="list-caption"><span>Участники</span><span>{d?fmt(d.total):'Загрузка…'}</span></div>{d?.partial&&<p className="muted">Список ещё загружается из Discord. Поиск по точному ID доступен сразу.</p>}<div className="member-list">{d?.items.map(m=><button key={m.id} className={selected===m.id?'chosen':''} onClick={()=>setSelected(m.id)}><img src={m.avatar} alt=""/><span><strong>{m.name}</strong><small>@{m.username}</small></span>{m.banned?<span className="tag danger">Бан</span>:m.timeout_until?<span className="tag">Таймаут</span>:<ChevronRight size={17}/>}</button>)}{d&&!d.items.length&&<Empty>Участники не найдены. Попробуй другой ник или ID.</Empty>}</div><Pager offset={offset} total={d?.total||0} size={30} onChange={setOffset}/></section><section className="panel member-detail">{!selected?<div className="select-member"><Users size={42}/><h2>Выбери участника</h2><p>Посмотри его роли, историю баланса<br/>и доступные действия.</p></div>:detail?.error?<LoadError data={detail}/>:detail?.member?<><div className="member-title"><img src={detail.member.avatar} alt=""/><div><h2>{detail.member.name}</h2><p>@{detail.member.username}</p><code>{detail.member.id}</code></div></div><div className="tags">{detail.member.roles.map(r=><span className="tag" key={r.id}>{r.name}</span>)}</div>{detail.member.timeout_until&&<p className="muted">Таймаут до {new Date(detail.member.timeout_until).toLocaleString('ru-RU')}</p>}{'balance'in detail&&<div className="balance-strip"><Coins/><span>Баланс казино</span><strong>{fmt(detail.balance)}<small> монет</small></strong></div>}<MemberActions member={detail.member} can={can} request={request}/>{detail.ledger&&<><h3>История баланса</h3><div className="ledger">{detail.ledger.map((r,i)=><div key={i}><span>{r.reason}<small>{date(r.created)}</small></span><strong className={r.delta>0?'positive':''}>{r.delta>0?'+':''}{fmt(r.delta)}</strong></div>)}{!detail.ledger.length&&<Empty>Операций ещё нет.</Empty>}</div></>}</>:<Empty>Загружаем карточку…</Empty>}</section></div>;
}
function MemberActions({member,can,request}){
 const [kind,setKind]=useState('timeout'),[reason,setReason]=useState(''),[seconds,setSeconds]=useState(600),[amount,setAmount]=useState(1000),[operation,setOperation]=useState('add');
 const options=['timeout','untimeout','ban','unban','coins'].filter(can);useEffect(()=>{if(!options.includes(kind))setKind(options[0]||'')},[member.id,options.join(',')]);
 if(!options.length)return null;
 return <form className="action-form" onSubmit={e=>{e.preventDefault();request({action:kind,target:member.id,reason,seconds:Number(seconds),amount:Number(amount),operation},`${labels[kind]} · ${member.name} · ID ${member.id}${kind==='coins'?` · ${{add:'Выдать',remove:'Снять',set:'Установить баланс'}[operation]} ${fmt(amount)} монет`:kind==='timeout'?` · ${fmt(Number(seconds)/60)} минут`:''}`)}}><h3>Действие с участником</h3><div className="form-grid"><Field label="Действие"><select value={kind} onChange={e=>setKind(e.target.value)}>{options.map(o=><option key={o} value={o}>{labels[o]}</option>)}</select></Field>{kind==='timeout'&&<Field label="Длительность"><select value={seconds} onChange={e=>setSeconds(e.target.value)}>{[[60,'1 минута'],[600,'10 минут'],[1800,'30 минут'],[3600,'1 час'],[86400,'1 день'],[604800,'7 дней'],[2419200,'28 дней']].map(([v,l])=><option key={v} value={v}>{l}</option>)}</select></Field>}{kind==='coins'&&<><Field label="Операция"><select value={operation} onChange={e=>setOperation(e.target.value)}><option value="add">Выдать</option><option value="remove">Снять</option><option value="set">Установить баланс</option></select></Field><Field label="Количество монет"><input type="number" min="0" max="1000000000" step="1" value={amount} onChange={e=>setAmount(e.target.value)} required/></Field></>}</div><Field label="Причина"><input value={reason} onChange={e=>setReason(e.target.value)} required maxLength={300} placeholder="Будет сохранена в журнале"/></Field><button className="primary" disabled={member.bot}>Продолжить <ArrowRight size={17}/></button></form>;
}
function Messages({can,request,version,fail}){
 const [kind,setKind]=useState(can('send_channel')?'send_channel':'send_dm'),[target,setTarget]=useState(''),[text,setText]=useState(''),[reason,setReason]=useState('');
 const d=useData(can('send_channel')?'channels':'overview',version,fail);if(d?.error)return <LoadError data={d}/>;
 return <div className="two-columns"><form className="panel" onSubmit={e=>{e.preventDefault();request({action:kind,target,text,reason},`Отправить сообщение от имени бота · ${kind==='send_dm'?'участнику':'в канал'} ${target}`)}}><h2>Сообщение от бота</h2><p className="muted">Весь введённый текст отправится без подписи сотрудника. Автор действия останется в аудите.</p><Field label="Куда отправить"><select value={kind} onChange={e=>{setKind(e.target.value);setTarget('')}}>{can('send_channel')&&<option value="send_channel">Канал сервера</option>}{can('send_dm')&&<option value="send_dm">Личные сообщения участника</option>}</select></Field><Field label={kind==='send_channel'?'Канал':'Discord ID участника'}>{kind==='send_channel'?<select required value={target} onChange={e=>setTarget(e.target.value)}><option value="">Выбери канал</option>{d?.items?.map(c=><option key={c.id} value={c.id}>#{c.name}</option>)}</select>:<input required pattern="[0-9]{15,20}" value={target} onChange={e=>setTarget(e.target.value)}/>}</Field><Field label="Текст"><textarea required maxLength={2000} rows={8} value={text} onChange={e=>setText(e.target.value)} placeholder="Напиши сообщение…"/></Field><div className="counter">{text.length} / 2000</div><Field label="Причина отправки"><input required maxLength={300} value={reason} onChange={e=>setReason(e.target.value)} placeholder="Например, объявление о событии"/></Field><button className="primary">Проверить и отправить <Send size={17}/></button></form><section className="panel preview-panel"><h2>Предпросмотр текста</h2><div className="bot-preview"><span className="avatar"><Mark/></span><div><strong>Фуфлыжник <span className="tag">БОТ</span></strong><p>{text||'Здесь появится твой текст.'}</p></div></div><p className="muted">Показываем исходный текст. Discord отобразит свою разметку. Упоминания не отправляют уведомления.</p></section></div>;
}
function Audit({version,fail}){
 const [filters,setFilters]=useState({}),[offset,setOffset]=useState(0);
 const query=new URLSearchParams({offset});Object.entries(filters).forEach(([k,v])=>{if(v)query.set(k,['from','to'].includes(k)?String(new Date(v).getTime()/1000):v)});
 const d=useData('audit?'+query,version,fail);
 const change=(k,v)=>{setFilters({...filters,[k]:v});setOffset(0)};
 function download(){const rows=[['Дата','Сотрудник','Действие','Участник','Причина','Результат'],...(d?.items||[]).map(r=>[date(r.created),r.actor,labels[r.action]||r.action,r.target,r.reason,r.status])];const csv='\ufeff'+rows.map(r=>r.map(v=>'"'+String(v??'').replace(/^[=+@-]/,"'").replaceAll('"','""')+'"').join(';')).join('\n');const url=URL.createObjectURL(new Blob([csv],{type:'text/csv;charset=utf-8'}));const a=document.createElement('a');a.href=url;a.download='audit-page.csv';a.click();URL.revokeObjectURL(url)}
 if(d?.error)return <LoadError data={d}/>;
 return <section className="panel"><div className="section-head"><h2>История действий</h2><button onClick={download} disabled={!d?.items.length}><Download size={16}/>Скачать страницу</button></div><div className="audit-filters"><Field label="Сотрудник"><input value={filters.actor||''} onChange={e=>change('actor',e.target.value)} placeholder="Логин"/></Field><Field label="Действие"><select value={filters.action||''} onChange={e=>change('action',e.target.value)}><option value="">Все действия</option>{Object.entries(labels).filter(([k])=>!['members','balance','accounts'].includes(k)).map(([k,l])=><option key={k} value={k}>{l}</option>)}</select></Field><Field label="Результат"><select value={filters.status||''} onChange={e=>change('status',e.target.value)}><option value="">Любой</option><option value="success">Выполнено</option><option value="error">Ошибка</option><option value="pending">Ожидание</option><option value="denied">Отказ</option></select></Field><Field label="Участник / канал"><input value={filters.target||''} onChange={e=>change('target',e.target.value)} placeholder="Discord ID"/></Field><Field label="С даты"><input type="datetime-local" value={filters.from||''} onChange={e=>change('from',e.target.value)}/></Field><Field label="До даты"><input type="datetime-local" value={filters.to||''} onChange={e=>change('to',e.target.value)}/></Field></div><AuditRows items={d?.items}/><Pager offset={offset} total={d?.total||0} size={50} onChange={setOffset}/></section>;
}
function Accounts({request,version,fail,user}){
 const d=useData('accounts',version,fail),blank={login:'',role:'support',discord_id:'',password:'',permissions:['members','timeout','untimeout'],enabled:true,reason:''};
 const [form,setForm]=useState(blank);const set=(k,v)=>setForm({...form,[k]:v});if(d?.error)return <LoadError data={d}/>;
 return <div className="two-columns"><section className="panel"><div className="section-head"><h2>Команда панели</h2><button onClick={()=>setForm(blank)}>Новый аккаунт</button></div><p className="muted">Эти роли управляют только сайтом. Discord-роли не меняются.</p>{d?.items.map(u=><button key={u.login} className="account-row" disabled={u.role==='tech'||(u.role==='owner'&&user.role!=='tech')} onClick={()=>setForm({...u,password:'',reason:''})}><span className="avatar">{u.login.slice(0,1).toUpperCase()}</span><span><strong>{u.login}</strong><small>{roles[u.role]}</small></span><span className="tag">{u.enabled?'Активен':'Отключён'}</span></button>)}</section><form className="panel" onSubmit={e=>{e.preventDefault();request({...form,action:'account'},`Сохранить аккаунт ${form.login} · ${roles[form.role]}`)}}><h2>Права сотрудника</h2><div className="form-grid"><Field label="Логин"><input required pattern="[a-z0-9_.-]{3,32}" value={form.login} onChange={e=>set('login',e.target.value)} autoComplete="off"/></Field><Field label="Роль на сайте"><select value={form.role} onChange={e=>setForm({...form,role:e.target.value,permissions:d?.defaults[e.target.value]||[]})}><option value="support">Саппорт</option><option value="moderator">Модератор</option>{user.role==='tech'&&<option value="owner">Овнер</option>}</select></Field><Field label="Discord ID"><input required pattern="[0-9]{15,20}" value={form.discord_id} onChange={e=>set('discord_id',e.target.value)}/></Field><Field label="Новый пароль"><input type="password" minLength={12} maxLength={256} value={form.password} onChange={e=>set('password',e.target.value)} autoComplete="new-password" placeholder="Пусто — оставить текущий"/></Field></div><div className="permissions">{d?.permissions.filter(p=>p!=='accounts'||form.role==='owner').map(p=><label key={p}><input type="checkbox" checked={form.permissions.includes(p)} onChange={e=>set('permissions',e.target.checked?[...form.permissions,p]:form.permissions.filter(v=>v!==p))}/>{labels[p]}</label>)}</div><label className="check-line"><input type="checkbox" checked={Boolean(form.enabled)} onChange={e=>set('enabled',e.target.checked)}/>Аккаунт включён</label><Field label="Причина изменения"><input required maxLength={300} value={form.reason} onChange={e=>set('reason',e.target.value)}/></Field><button className="primary">Сохранить доступ <Check size={17}/></button></form></div>;
}
function Console({request,fail}){
 const [text,setText]=useState('');
 function submit(e){e.preventDefault();try{const [command,target,...args]=text.trim().split(/\s+/);let body={action:command,target};if(command==='timeout'){body.seconds=Number(args.shift())*60}else if(command==='coins'){body.operation=args.shift();body.amount=Number(args.shift())}if(!['timeout','untimeout','ban','unban','coins'].includes(command)||!/^\d{15,20}$/.test(target))throw new Error('Выбери команду из примеров и укажи Discord ID.');body.reason=args.join(' ');if(!body.reason)throw new Error('В конце команды укажи причину.');request(body,text)}catch(e){fail(e)}}
 return <section className="panel console"><Terminal size={28}/><h2>Быстрая консоль</h2><p className="muted">Те же права и подтверждения, что в карточке участника.</p><pre>timeout ID 10 причина<br/>untimeout ID причина<br/>ban ID причина<br/>unban ID причина<br/>coins ID add 1000 причина<br/>coins ID remove 1000 причина<br/>coins ID set 1000 причина</pre><form onSubmit={submit}><Field label="Команда"><input value={text} onChange={e=>setText(e.target.value)} placeholder="timeout 123456789012345678 10 нарушение правил" required maxLength={450}/></Field><button className="primary">Проверить команду <ArrowRight size={17}/></button></form></section>;
}
function SettingsPage({request,user}){return <section className="panel settings"><h2>Твой аккаунт</h2><p>{user.login} · {roles[user.role]}</p><p className="muted">После смены пароля все сеансы этого аккаунта завершатся.</p><form onSubmit={e=>{e.preventDefault();const data=Object.fromEntries(new FormData(e.currentTarget));request({action:'password',...data},'Изменить пароль и завершить все сеансы?')}}><Field label="Текущий пароль"><input type="password" name="old_password" autoComplete="current-password" required/></Field><Field label="Новый пароль"><input type="password" name="password" autoComplete="new-password" minLength={12} maxLength={256} required/></Field><button className="primary">Изменить пароль <LockKeyhole size={17}/></button></form></section>}
createRoot(document.getElementById('root')).render(<App/>);
