// AdminUsers — search + detail drawer with balance-credit and VIP toggle. const { useState: useAdmUState, useEffect: useAdmUEffect } = React; function AdminUsers({ onNavigate }) { const [q, setQ] = useAdmUState(''); const [items, setItems] = useAdmUState([]); const [loading, setLoading] = useAdmUState(true); const [selected, setSelected] = useAdmUState(null); const load = async (search) => { setLoading(true); try { const data = await api.get('/api/admin/users?page=1&page_size=50' + (search ? `&q=${encodeURIComponent(search)}` : '')); if (!data.__unauthorized) setItems(data.items || []); } catch (_) {} finally { setLoading(false); } }; useAdmUEffect(() => { load(''); }, []); return (

Пользователи

setQ(e.target.value)} onKeyDown={e => e.key === 'Enter' && load(q)} style={{ flex: 1 }} />
{loading ?
Загрузка...
: (
{items.map(u => ( setSelected(u.user_id)}> ))} {items.length === 0 && ( )}
#UsernameИмяБалансVIPРегистрация
#{u.user_id} {u.user_name ? '@' + u.user_name : '—'} {u.first_name || '—'} {u.balance.toLocaleString('ru-RU')} ₽ {u.is_vip ? '⭐' : ''} {formatDate(u.reg_date) || '—'}
Ничего не нашли
) } {selected && ( setSelected(null)} onUserChanged={() => load(q)} /> )}
); } function AdminUserDrawer({ userId, onClose, onUserChanged }) { const [data, setData] = useAdmUState(null); const [delta, setDelta] = useAdmUState(1000); const [reason, setReason] = useAdmUState(''); const [busy, setBusy] = useAdmUState(false); const [error, setError] = useAdmUState(''); const [okMsg, setOkMsg] = useAdmUState(''); const reload = async () => { try { const fresh = await api.get('/api/admin/users/' + userId); if (!fresh.__unauthorized) setData(fresh); } catch (e) { setError(e.message || 'Ошибка загрузки'); } }; useAdmUEffect(() => { reload(); }, [userId]); const credit = async () => { if (!delta || delta <= 0) return setError('Сумма должна быть > 0'); if (!reason.trim()) return setError('Укажите причину начисления'); setBusy(true); setError(''); setOkMsg(''); try { const res = await api.post('/api/admin/users/' + userId + '/balance', { delta: Number(delta), reason: reason.trim() }); setOkMsg(`Начислено ${res.balance_after - res.balance_before} ₽. Новый баланс: ${res.balance_after} ₽.`); setReason(''); await reload(); onUserChanged && onUserChanged(); } catch (e) { setError(e.message || 'Ошибка'); } finally { setBusy(false); } }; const toggleVip = async () => { if (!data) return; setBusy(true); setError(''); try { await api.post('/api/admin/users/' + userId + '/vip', { is_vip: !data.is_vip }); await reload(); onUserChanged && onUserChanged(); } catch (e) { setError(e.message || 'Ошибка'); } finally { setBusy(false); } }; return (
e.stopPropagation()} >

Пользователь #{userId}

{!data ?
Загрузка...
: ( <>
{data.first_name || '—'} {data.user_name ? '· @' + data.user_name : ''}
Регистрация: {formatDate(data.reg_date) || '—'}
Баланс: {data.balance.toLocaleString('ru-RU')} ₽ · VIP: {data.is_vip ? '⭐ да' : 'нет'}
Привязки: {data.providers.length === 0 ? 'нет' : data.providers.map(p => `${p.provider}:${p.identifier}`).join(', ')}
{error &&
{error}
} {okMsg &&
{okMsg}
}

Ручное пополнение

setDelta(Number(e.target.value))} />
setReason(e.target.value)} />

VIP

Последние заказы

{data.recent_orders.length === 0 ?
Заказов нет
: data.recent_orders.map(o => (
#{o.order_id} · {o.position_name} · {o.status} {o.price.toLocaleString('ru-RU')} ₽
)) }
)}
); } Object.assign(window, { AdminUsers });