// screens-users.jsx — User detail, member tiers, agents
// UserScreen / LevelsScreen 已接真后端：
//   - /api/users (list)
//   - /api/wallet/{userId}/adjust
//   - /api/admin/devices  (filter by user via accounts[])
//   - /api/admin/events/user/{userID}
//   - /api/risk/blacklist (POST / DELETE / GET)
//   - /api/risk/check (POST)
//   - /api/audit (filter by user)
//   - /api/membership/leaderboard
// AgentsScreen 仍为 mock — TODO 后端缺代理 API。

const _NA = window.NebulaAdmin;
const _adminFetch  = _NA.adminFetch;
const _useAdminPoll = _NA.useAdminPoll;

// ─── helpers (local — Babel scope is per-file) ───────────────────────────────
function _relTime(iso) {
  if (!iso) return '—';
  const ms = Date.now() - new Date(iso).getTime();
  if (Number.isNaN(ms)) return iso;
  const s = Math.floor(ms / 1000);
  // Suffix kept language-neutral (s/m/h/d ago-equivalent) — short unit + space-tolerant.
  if (s < 60)    return s + 's';
  if (s < 3600)  return Math.floor(s / 60) + 'm';
  if (s < 86400) return Math.floor(s / 3600) + 'h';
  return Math.floor(s / 86400) + 'd';
}
function _shortId(id, n) { n = n || 12; if (!id) return ''; return String(id).length > n ? String(id).slice(0, n) + '…' : String(id); }
function _kindColor(k) {
  if (k === 'login')    return 'oklch(0.6 0.16 245)';
  if (k === 'recharge' || k === 'deposit') return 'oklch(0.6 0.16 145)';
  if (k === 'withdraw') return 'oklch(0.65 0.14 30)';
  if (k === 'bet' || k === 'wager') return 'oklch(0.7 0.14 85)';
  if (k === 'win')      return 'oklch(0.55 0.18 305)';
  return 'var(--text-muted)';
}
function _tierFromRecharge(amt) {
  amt = Number(amt) || 0;
  if (amt >= 800000) return 'supreme';
  if (amt >= 200000) return 'diamond';
  if (amt >= 40000)  return 'gold';
  if (amt >= 8000)   return 'silver';
  if (amt >= 1000)   return 'bronze';
  return 'normal';
}

// ─────────────────────────────────────────────────────────────────────────────
// Phase-1 helpers: masking, online detection, role/session, column persistence
// ─────────────────────────────────────────────────────────────────────────────
function _maskName(n) {
  if (!n) return '—';
  const s = String(n);
  if (s.length <= 1) return s;
  if (s.length === 2) return s[0] + '*';
  return s[0] + '*'.repeat(Math.max(1, s.length - 2)) + s[s.length - 1];
}
function _maskPhone(p) {
  if (!p) return '—';
  const s = String(p);
  if (s.includes('****')) return s; // already masked by backend
  if (s.length < 7) return s;
  return s.slice(0, 3) + '****' + s.slice(-4);
}
function _maskEmail(e) {
  if (!e) return '—';
  const s = String(e);
  const at = s.indexOf('@');
  if (at < 0) return s;
  const user = s.slice(0, at);
  const dom = s.slice(at);
  if (user.length <= 3) return user[0] + '***' + dom;
  return user.slice(0, 3) + '***' + dom;
}
function _isOnline(u) {
  if (!u) return false;
  const lb = u.last_bet || u.last_seen_at || u.last_login_at;
  if (!lb) return false;
  return (Date.now() - new Date(lb).getTime()) < 5 * 60 * 1000;
}
function _currentSessionRole() {
  try {
    const u = JSON.parse(localStorage.getItem('nebula.admin.user') || 'null');
    return (u && u.role) || 'admin';
  } catch (e) { return 'admin'; }
}
function _loadCols() {
  try {
    const raw = localStorage.getItem('nebula.admin.users.cols');
    if (raw) return JSON.parse(raw);
  } catch (e) {}
  return null;
}
function _saveCols(v) {
  try { localStorage.setItem('nebula.admin.users.cols', JSON.stringify(v)); } catch (e) {}
}
function _logTodo(action, user, extra) {
  // Phase-1 stub — Phase-2 will POST to /api/admin/approvals
  // eslint-disable-next-line no-console
  console.log('[TODO approval]', action, user && user.id, extra || '');
}
function _exportCSV(rows, columns, filename) {
  const head = columns.map(c => '"' + c.label.replace(/"/g, '""') + '"').join(',');
  const body = rows.map(r => columns.map(c => {
    const v = c.value(r);
    if (v == null) return '""';
    return '"' + String(v).replace(/"/g, '""') + '"';
  }).join(',')).join('\n');
  const blob = new Blob(['﻿' + head + '\n' + body], { type: 'text/csv;charset=utf-8' });
  const url = URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = url; a.download = filename || ('users-' + Date.now() + '.csv');
  document.body.appendChild(a); a.click();
  setTimeout(() => { URL.revokeObjectURL(url); a.remove(); }, 100);
}

// 30-column registry. `real` = backed by /api/users field today; otherwise `—` + todo attr.
function _userColumns(t) {
  return [
    { key: 'id',          label: t.tx('会员ID', 'Member ID'),      real: true,  width: 110, value: (u) => u.id,
      render: (u) => <span className="text-mono" style={{ fontSize: 11.5 }}>{u.id}</span> },
    { key: 'account',     label: t.tx('账号', 'Account'),          real: false, width: 110, value: () => '',
      render: () => <span data-todo="endpoint:/api/admin/users/{id}/profile">—</span> },
    { key: 'name',        label: t.tx('昵称', 'Nickname'),          real: true,  width: 130, value: (u) => u.name,
      render: (u) => <span>{u.name || '—'}</span> },
    { key: 'real_name',   label: t.tx('真实姓名', 'Real name'),     real: false, width: 90,  value: () => '',
      render: () => <span data-todo="endpoint:/api/admin/users/{id}/profile">—</span> },
    { key: 'phone',       label: t.tx('手机号', 'Phone'),           real: true,  width: 130, value: (u) => _maskPhone(u.phone),
      render: (u) => <span className="text-mono" style={{ fontSize: 11 }}>{_maskPhone(u.phone)}</span> },
    { key: 'email',       label: t.tx('邮箱', 'Email'),             real: false, width: 160, value: () => '',
      render: () => <span data-todo="endpoint:/api/admin/users/{id}/profile">—</span> },
    { key: 'tenant',      label: t.tx('所属租户', 'Tenant'),        real: false, width: 90,  value: () => 'nebula',
      render: () => <span className="badge outline" style={{ fontSize: 10 }} data-todo="endpoint:/api/admin/users/{id}/profile">nebula</span> },
    { key: 'site',        label: t.tx('所属站点', 'Site'),          real: false, width: 90,  value: () => 'main',
      render: () => <span className="badge outline" style={{ fontSize: 10 }} data-todo="endpoint:/api/admin/users/{id}/profile">main</span> },
    { key: 'tier',        label: t.tx('会员等级', 'Tier'),          real: true,  width: 80,  value: (u) => _tierFromRecharge(u.total_stake),
      render: (u) => <TierBadge tier={_tierFromRecharge(u.total_stake)} t={t} /> },
    { key: 'vip',         label: t.tx('VIP等级', 'VIP'),            real: false, width: 60,  value: () => '',
      render: () => <span data-todo="endpoint:/api/admin/users/{id}/vip">—</span> },
    { key: 'status',      label: t.tx('账号状态', 'Status'),        real: false, width: 80,  value: (u, ctx) => ctx && ctx.blackSet && ctx.blackSet.has(u.id) ? '冻结' : '正常',
      render: (u, ctx) => ctx && ctx.blackSet && ctx.blackSet.has(u.id)
        ? <span className="badge danger" style={{ fontSize: 10 }}>{t.tx('冻结', 'Frozen')}</span>
        : <span className="badge success" style={{ fontSize: 10 }}>{t.tx('正常', 'Normal')}</span> },
    { key: 'risk',        label: t.tx('风险等级', 'Risk'),          real: false, width: 80,  value: (u, ctx) => ctx && ctx.blackSet && ctx.blackSet.has(u.id) ? 'freeze' : 'safe',
      render: (u, ctx) => {
        const lvl = ctx && ctx.blackSet && ctx.blackSet.has(u.id) ? 'freeze' : 'safe';
        const cls = lvl === 'freeze' ? 'danger' : lvl === 'limit' ? 'warning' : lvl === 'watch' ? 'info' : 'success';
        return <span className={'badge ' + cls} style={{ fontSize: 10 }} data-todo="endpoint:/api/risk/check">{lvl}</span>;
      } },
    { key: 'currency',    label: t.tx('币种', 'Currency'),          real: false, width: 60,  value: () => 'CNY',
      render: () => <span className="text-mono" style={{ fontSize: 11 }} data-todo="endpoint:/api/admin/users/{id}/wallet">CNY</span> },
    { key: 'balance',     label: t.tx('余额', 'Balance'),           real: true,  width: 110, value: (u) => u.balance || 0,
      render: (u) => <span className="tabular" style={{ fontSize: 11.5 }}>{fmtCN(u.balance || 0)}</span> },
    { key: 'frozen',      label: t.tx('冻结金额', 'Frozen'),        real: false, width: 90,  value: () => '',
      render: () => <span data-todo="endpoint:/api/admin/users/{id}/wallet">—</span> },
    { key: 'total_dep',   label: t.tx('累计充值', 'Total deposit'), real: false, width: 110, value: () => '',
      render: () => <span data-todo="endpoint:/api/admin/users/{id}/wallet">—</span> },
    { key: 'total_wd',    label: t.tx('累计提现', 'Total withdraw'),real: false, width: 110, value: () => '',
      render: () => <span data-todo="endpoint:/api/admin/users/{id}/wallet">—</span> },
    { key: 'total_stake', label: t.tx('累计投注', 'Total stake'),   real: true,  width: 110, value: (u) => u.total_stake || 0,
      render: (u) => <span className="tabular" style={{ fontSize: 11.5 }}>{fmtCN(u.total_stake || 0)}</span> },
    { key: 'reg_source',  label: t.tx('注册来源', 'Reg source'),    real: false, width: 90,  value: () => '',
      render: () => <span data-todo="endpoint:/api/admin/users/{id}/profile">—</span> },
    { key: 'reg_channel', label: t.tx('注册渠道', 'Reg channel'),   real: false, width: 90,  value: () => '',
      render: () => <span data-todo="endpoint:/api/admin/users/{id}/profile">—</span> },
    { key: 'created_at',  label: t.tx('注册时间', 'Registered'),    real: true,  width: 130, value: (u) => (u.created_at || '').slice(0, 19).replace('T', ' '),
      render: (u) => <span className="text-faint" style={{ fontSize: 11 }}>{(u.created_at || '').slice(0, 16).replace('T', ' ') || '—'}</span> },
    { key: 'last_login',  label: t.tx('最后登录时间', 'Last login'),real: true,  width: 110, value: (u) => u.last_bet || '',
      render: (u) => <span className="text-faint" style={{ fontSize: 11 }}>{u.last_bet ? _relTime(u.last_bet) : '—'}</span> },
    { key: 'last_ip',     label: t.tx('最后登录IP', 'Last IP'),     real: false, width: 110, value: () => '',
      render: () => <span data-todo="endpoint:/api/admin/devices?user={id}">—</span> },
    { key: 'last_region', label: t.tx('最后登录地区', 'Last region'),real: false, width: 110, value: () => '',
      render: () => <span data-todo="endpoint:/api/admin/devices?user={id}">—</span> },
    { key: 'device_model',label: t.tx('手机型号', 'Phone model'),   real: false, width: 110, value: () => '',
      render: () => <span data-todo="endpoint:/api/admin/devices?user={id}">—</span> },
    { key: 'device_id',   label: t.tx('设备ID', 'Device ID'),       real: false, width: 130, value: () => '',
      render: () => <span data-todo="endpoint:/api/admin/devices?user={id}">—</span> },
    { key: 'online',      label: t.tx('在线状态', 'Online'),        real: true,  width: 70,  value: (u) => _isOnline(u) ? 'online' : 'offline',
      render: (u) => _isOnline(u)
        ? <span className="badge success" style={{ fontSize: 10 }}>{t.tx('在线', 'Online')}</span>
        : <span className="badge outline" style={{ fontSize: 10 }}>{t.tx('离线', 'Offline')}</span> },
    { key: 'online_total',label: t.tx('累计在线时长', 'Total online'), real: false, width: 100, value: () => '',
      render: () => <span data-todo="endpoint:/api/admin/users/{id}/online-stats">—</span> },
    { key: 'actions',     label: t.tx('操作', 'Actions'),           real: true,  width: 110, value: () => '',
      render: null /* rendered specially */ },
  ];
}

const DEFAULT_VISIBLE_COLS = ['id', 'name', 'phone', 'tier', 'status', 'risk', 'balance', 'total_stake', 'created_at', 'last_login', 'online', 'actions'];

// 14-tab definition (display order = render order)
function _userTabs(t) {
  return [
    ['overview', t.tx('概览', 'Overview')],
    ['profile',  t.tx('基础资料', 'Profile')],
    ['devices',  t.tx('登录与设备', 'Login & Devices')],
    ['tier',     t.tx('会员等级', 'Tier')],
    ['wallet',   t.tx('钱包与账本', 'Wallet & Ledger')],
    ['orders',   t.tx('充值/提现', 'Deposit/Withdraw')],
    ['promo',    t.tx('活动/任务', 'Promo/Tasks')],
    ['content',  t.tx('内容/视频', 'Content/Video')],
    ['live',     t.tx('直播行为', 'Live behavior')],
    ['bets',     t.tx('游戏/投注', 'Games/Bets')],
    ['referral', t.tx('代理/邀请', 'Referral')],
    ['risk',     t.tx('风控记录', 'Risk records')],
    ['support',  t.tx('客服/工单', 'Support/Tickets')],
    ['audit',    t.tx('操作日志', 'Audit log')],
  ];
}

// Role-based tab access matrix
const TAB_ACCESS = {
  admin:   ['overview','profile','devices','tier','wallet','orders','promo','content','live','bets','referral','risk','support','audit'],
  finance: ['overview','profile','wallet','orders','audit'],
  support: ['overview','profile','orders','promo','support','audit'],
  ops:     ['overview','profile','tier','promo','content','live','support','audit'],
  risk:    ['overview','profile','devices','risk','audit'],
};

// Quick-action access matrix (button-level role gate)
const ACTION_ACCESS = {
  freeze:        ['admin', 'ops', 'risk'],
  unfreeze:      ['admin', 'ops', 'risk'],
  limit_login:   ['admin', 'ops', 'risk'],
  limit_deposit: ['admin', 'ops', 'risk'],
  limit_withdraw:['admin', 'ops', 'risk'],
  limit_bet:     ['admin', 'ops', 'risk'],
  force_logout:  ['admin', 'risk'],
  reset_pw:      ['admin', 'support'],
  change_tier:   ['admin', 'ops'],
  add_tag:       ['admin', 'ops'],
  adjust:        ['admin', 'finance'],
  freeze_funds:  ['admin', 'finance'],
  reveal:        ['admin'],
};

// ─────────────────────────────────────────────────────────────────────────────
// 07 USER DETAIL — 列表 + 详情右抽屉（14 tab · 角色分视图）
// ─────────────────────────────────────────────────────────────────────────────
function UserScreen({ t, push }) {
  const [users, err] = _useAdminPoll('/api/users', 10000, true);
  const [selectedId, setSelectedId] = React.useState(null);
  const [drawerOpen, setDrawerOpen] = React.useState(false);
  const [tab, setTab] = React.useState('overview');
  const [blacklist, setBlacklist] = React.useState([]);
  const [blReloadTick, setBlReloadTick] = React.useState(0);

  // ── Filter state (16) ─────────────────────────────────────────────────────
  const [filterOpen, setFilterOpen] = React.useState(true);
  const [fTenant, setFTenant] = React.useState('');
  const [fSite, setFSite] = React.useState('');
  const [fTier, setFTier] = React.useState('');
  const [fVip, setFVip] = React.useState('');
  const [fStatus, setFStatus] = React.useState('');
  const [fRisk, setFRisk] = React.useState('');
  const [fCurrency, setFCurrency] = React.useState('');
  const [fRegFrom, setFRegFrom] = React.useState('');
  const [fRegTo, setFRegTo] = React.useState('');
  const [fLoginFrom, setFLoginFrom] = React.useState('');
  const [fLoginTo, setFLoginTo] = React.useState('');
  const [fChannel, setFChannel] = React.useState('');
  const [fSearch, setFSearch] = React.useState('');
  const [fDevice, setFDevice] = React.useState('');
  const [fIp, setFIp] = React.useState('');
  const [fFirstDep, setFFirstDep] = React.useState('all'); // yes | no | all
  const [fBlack, setFBlack] = React.useState('all');
  const [fOnline, setFOnline] = React.useState('all');

  // ── Column visibility ────────────────────────────────────────────────────
  const allCols = React.useMemo(() => _userColumns(t), [t.lang]);
  const [visibleCols, setVisibleCols] = React.useState(() => {
    const saved = _loadCols();
    return saved || DEFAULT_VISIBLE_COLS;
  });
  const [colSetOpen, setColSetOpen] = React.useState(false);

  React.useEffect(() => { _saveCols(visibleCols); }, [visibleCols]);

  const cols = allCols.filter(c => visibleCols.includes(c.key));

  // Load blacklist
  React.useEffect(() => {
    let cancelled = false;
    (async () => {
      try {
        const d = await _adminFetch('/api/risk/blacklist?kind=user');
        if (!cancelled) setBlacklist((d && d.entries) || []);
      } catch (e) {
        if (!cancelled) setBlacklist([]);
      }
    })();
    return () => { cancelled = true; };
  }, [blReloadTick]);

  const blackSet = React.useMemo(() => {
    const s = new Set();
    blacklist.forEach(b => s.add(b.value));
    return s;
  }, [blacklist]);

  const list = (users && users.users) || [];

  // ── Apply 16 filters client-side ─────────────────────────────────────────
  const filtered = React.useMemo(() => {
    const q = fSearch.trim().toLowerCase();
    return list.filter(u => {
      if (q) {
        const hay = `${u.id} ${u.name || ''} ${u.phone || ''}`.toLowerCase();
        if (!hay.includes(q)) return false;
      }
      if (fTier) {
        if (_tierFromRecharge(u.total_stake) !== fTier) return false;
      }
      if (fStatus) {
        const isFrozen = blackSet.has(u.id);
        if (fStatus === 'frozen' && !isFrozen) return false;
        if (fStatus === 'normal' && isFrozen) return false;
        if ((fStatus === 'limit' || fStatus === 'lock') && !isFrozen) return false; // no real backend signal yet
      }
      if (fRisk) {
        const lvl = blackSet.has(u.id) ? 'freeze' : 'safe';
        if (fRisk !== lvl) return false;
      }
      if (fRegFrom) {
        if (!u.created_at || u.created_at.slice(0, 10) < fRegFrom) return false;
      }
      if (fRegTo) {
        if (!u.created_at || u.created_at.slice(0, 10) > fRegTo) return false;
      }
      if (fLoginFrom) {
        if (!u.last_bet || u.last_bet.slice(0, 10) < fLoginFrom) return false;
      }
      if (fLoginTo) {
        if (!u.last_bet || u.last_bet.slice(0, 10) > fLoginTo) return false;
      }
      if (fBlack !== 'all') {
        const b = blackSet.has(u.id);
        if (fBlack === 'yes' && !b) return false;
        if (fBlack === 'no' && b) return false;
      }
      if (fOnline !== 'all') {
        const o = _isOnline(u);
        if (fOnline === 'yes' && !o) return false;
        if (fOnline === 'no' && o) return false;
      }
      if (fFirstDep !== 'all') {
        // No backend signal — treat balance>0 || bets>0 as "had a deposit"
        const seems = (u.balance || 0) > 0 || (u.bets_count || 0) > 0;
        if (fFirstDep === 'yes' && !seems) return false;
        if (fFirstDep === 'no' && seems) return false;
      }
      // fTenant / fSite / fVip / fCurrency / fChannel / fDevice / fIp — no
      // backend signal yet; we let everything through and rely on Phase-2
      // backed fields. Annotating as TODO inline.
      return true;
    });
  }, [list, fSearch, fTier, fStatus, fRisk, fRegFrom, fRegTo, fLoginFrom, fLoginTo, fBlack, fOnline, fFirstDep, blackSet]);

  const resetFilters = () => {
    setFTenant(''); setFSite(''); setFTier(''); setFVip(''); setFStatus(''); setFRisk('');
    setFCurrency(''); setFRegFrom(''); setFRegTo(''); setFLoginFrom(''); setFLoginTo('');
    setFChannel(''); setFSearch(''); setFDevice(''); setFIp('');
    setFFirstDep('all'); setFBlack('all'); setFOnline('all');
  };

  const exportCsv = () => {
    const exportCols = cols.filter(c => c.key !== 'actions').map(c => ({
      label: c.label,
      value: (u) => c.value(u, { blackSet }),
    }));
    _exportCSV(filtered, exportCols, `users-${new Date().toISOString().slice(0, 10)}.csv`);
    push(t.tx(`已导出 ${filtered.length} 行`, `Exported ${filtered.length} rows`));
  };

  const selected = list.find(u => u.id === selectedId) || null;
  const reloadBlacklist = () => setBlReloadTick(x => x + 1);

  const openDrawer = (u) => { setSelectedId(u.id); setDrawerOpen(true); setTab('overview'); };
  const closeDrawer = () => setDrawerOpen(false);

  return (
    <div className="stack">
      <div className="page-h">
        <div>
          <h1>{t.p_users}</h1>
          <div className="sub">
            {list.length} {t.tx('名真实用户', 'real users')} · {t.tx('黑名单', 'Blacklist')} {blacklist.length} · {t.tx('命中筛选', 'Filtered')} {filtered.length}
            {err && <span style={{ color: 'var(--danger)', marginLeft: 8 }}>· {t.tx('API 错误', 'API error')} {err.message}</span>}
          </div>
        </div>
        <div className="actions">
          <button className="btn btn-sm" onClick={() => setColSetOpen(true)}><Icons.cog /> {t.tx('列设置', 'Columns')}</button>
          <button className="btn btn-sm" onClick={exportCsv}><Icons.download /> {t.tx('导出 CSV', 'Export CSV')}</button>
        </div>
      </div>

      {/* ── Filter bar (16 conditions, collapsible) ─────────────────────── */}
      <div className="card" style={{ padding: 12 }}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: filterOpen ? 10 : 0 }}>
          <button className="btn btn-xs btn-ghost" onClick={() => setFilterOpen(!filterOpen)}>
            {filterOpen ? '▾' : '▸'} {t.tx('筛选', 'Filters')}
          </button>
          <span className="text-faint" style={{ fontSize: 11 }}>
            {t.tx('16 个条件 · 客户端过滤', '16 conditions · client-side filtering')}
          </span>
          <div style={{ flex: 1 }} />
          <button className="btn btn-xs btn-ghost" onClick={resetFilters}>{t.tx('重置筛选', 'Reset')}</button>
        </div>
        {filterOpen && (
          <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(180px, 1fr))', gap: 8 }}>
            <select className="select" value={fTenant} onChange={(e) => setFTenant(e.target.value)} data-todo="endpoint:/api/admin/tenants">
              <option value="">{t.tx('全部租户', 'All tenants')}</option>
              <option value="nebula">nebula</option>
            </select>
            <select className="select" value={fSite} onChange={(e) => setFSite(e.target.value)} data-todo="endpoint:/api/admin/sites">
              <option value="">{t.tx('全部站点', 'All sites')}</option>
              <option value="main">main</option>
            </select>
            <select className="select" value={fTier} onChange={(e) => setFTier(e.target.value)}>
              <option value="">{t.tx('全部等级', 'All tiers')}</option>
              <option value="normal">normal</option>
              <option value="bronze">bronze</option>
              <option value="silver">silver</option>
              <option value="gold">gold</option>
              <option value="diamond">diamond</option>
              <option value="supreme">supreme</option>
            </select>
            <select className="select" value={fVip} onChange={(e) => setFVip(e.target.value)} data-todo="endpoint:/api/admin/users/{id}/vip">
              <option value="">{t.tx('全部 VIP', 'All VIP')}</option>
              {[0,1,2,3,4,5,6,7,8,9,10].map(v => <option key={v} value={v}>VIP{v}</option>)}
            </select>
            <select className="select" value={fStatus} onChange={(e) => setFStatus(e.target.value)}>
              <option value="">{t.tx('全部状态', 'All status')}</option>
              <option value="normal">{t.tx('正常', 'Normal')}</option>
              <option value="frozen">{t.tx('冻结', 'Frozen')}</option>
              <option value="limit">{t.tx('限制', 'Limited')}</option>
              <option value="lock">{t.tx('锁定', 'Locked')}</option>
            </select>
            <select className="select" value={fRisk} onChange={(e) => setFRisk(e.target.value)}>
              <option value="">{t.tx('全部风险', 'All risk')}</option>
              <option value="safe">safe</option>
              <option value="watch">watch</option>
              <option value="limit">limit</option>
              <option value="freeze">freeze</option>
            </select>
            <select className="select" value={fCurrency} onChange={(e) => setFCurrency(e.target.value)} data-todo="endpoint:/api/admin/users/{id}/wallet">
              <option value="">{t.tx('全部币种', 'All currencies')}</option>
              <option value="CNY">CNY</option>
              <option value="USDT">USDT</option>
              <option value="USD">USD</option>
            </select>
            <input className="input" placeholder={t.tx('注册渠道', 'Reg channel')} value={fChannel} onChange={(e) => setFChannel(e.target.value)} data-todo="endpoint:/api/admin/users/{id}/profile" />
            <input className="input" placeholder={t.tx('手机/账号/会员ID', 'Phone / Account / ID')} value={fSearch} onChange={(e) => setFSearch(e.target.value)} />
            <input className="input" placeholder={t.tx('设备ID', 'Device ID')} value={fDevice} onChange={(e) => setFDevice(e.target.value)} data-todo="endpoint:/api/admin/devices" />
            <input className="input" placeholder={t.tx('IP 地址', 'IP address')} value={fIp} onChange={(e) => setFIp(e.target.value)} data-todo="endpoint:/api/admin/devices" />
            <select className="select" value={fFirstDep} onChange={(e) => setFFirstDep(e.target.value)}>
              <option value="all">{t.tx('是否首充 · 全部', 'First deposit · all')}</option>
              <option value="yes">{t.tx('已首充', 'Has first deposit')}</option>
              <option value="no">{t.tx('未首充', 'No first deposit')}</option>
            </select>
            <select className="select" value={fBlack} onChange={(e) => setFBlack(e.target.value)}>
              <option value="all">{t.tx('是否黑名单 · 全部', 'Blacklist · all')}</option>
              <option value="yes">{t.tx('黑名单', 'Blacklisted')}</option>
              <option value="no">{t.tx('非黑名单', 'Not blacklisted')}</option>
            </select>
            <select className="select" value={fOnline} onChange={(e) => setFOnline(e.target.value)}>
              <option value="all">{t.tx('是否在线 · 全部', 'Online · all')}</option>
              <option value="yes">{t.tx('在线', 'Online')}</option>
              <option value="no">{t.tx('离线', 'Offline')}</option>
            </select>
            <div style={{ gridColumn: '1 / -1', display: 'flex', flexDirection: 'column', gap: 4 }}>
              <div className="text-faint" style={{ fontSize: 10 }}>{t.tx('注册时间', 'Registered')}</div>
              <DateRangePicker from={fRegFrom} to={fRegTo} onChange={(f, tt) => { setFRegFrom(f || ''); setFRegTo(tt || ''); }} t={t} size="sm" />
            </div>
            <div style={{ gridColumn: '1 / -1', display: 'flex', flexDirection: 'column', gap: 4 }}>
              <div className="text-faint" style={{ fontSize: 10 }}>{t.tx('最后登录时间', 'Last login')}</div>
              <DateRangePicker from={fLoginFrom} to={fLoginTo} onChange={(f, tt) => { setFLoginFrom(f || ''); setFLoginTo(tt || ''); }} t={t} size="sm" />
            </div>
          </div>
        )}
      </div>

      {/* ── List table ─────────────────────────────────────────────────── */}
      <div className="card">
        <div className="tbl-scroll">
          <table className="tbl">
            <thead>
              <tr>
                {cols.map(c => (
                  <th key={c.key} style={{ width: c.width, minWidth: c.width, whiteSpace: 'nowrap' }}>{c.label}</th>
                ))}
              </tr>
            </thead>
            <tbody>
              {!users && <tr><td colSpan={cols.length} style={{ textAlign: 'center', padding: 24, color: 'var(--text-muted)' }}>{t.tx('加载中…', 'Loading…')}</td></tr>}
              {users && filtered.length === 0 && <tr><td colSpan={cols.length} style={{ textAlign: 'center', padding: 24, color: 'var(--text-muted)' }}>{t.tx('无匹配用户', 'No matching users')}</td></tr>}
              {filtered.map(u => (
                <tr key={u.id} style={{ cursor: 'pointer' }} onClick={() => openDrawer(u)}>
                  {cols.map(c => (
                    <td key={c.key} style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', maxWidth: c.width }}>
                      {c.key === 'actions'
                        ? <button className="btn btn-xs" onClick={(e) => { e.stopPropagation(); openDrawer(u); }}>{t.tx('详情', 'Details')}</button>
                        : c.render(u, { blackSet, t })}
                    </td>
                  ))}
                </tr>
              ))}
            </tbody>
          </table>
        </div>
      </div>

      {/* Column-settings modal */}
      {colSetOpen && (
        <Modal open={true} onClose={() => setColSetOpen(false)} title={t.tx('列设置', 'Column settings')} width={520}
               footer={<>
                 <button className="btn btn-sm" onClick={() => setVisibleCols(DEFAULT_VISIBLE_COLS)}>{t.tx('恢复默认', 'Reset to default')}</button>
                 <button className="btn btn-sm" onClick={() => setVisibleCols(allCols.map(c => c.key))}>{t.tx('全选', 'Select all')}</button>
                 <button className="btn btn-pri btn-sm" onClick={() => setColSetOpen(false)}>{t.tx('完成', 'Done')}</button>
               </>}>
          <div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: 6, fontSize: 12.5 }}>
            {allCols.map(c => (
              <label key={c.key} style={{ display: 'flex', alignItems: 'center', gap: 6, cursor: 'pointer' }}>
                <input type="checkbox"
                       checked={visibleCols.includes(c.key)}
                       onChange={(e) => {
                         setVisibleCols(prev =>
                           e.target.checked ? [...prev, c.key] : prev.filter(k => k !== c.key)
                         );
                       }} />
                <span>{c.label}</span>
                {!c.real && <span className="badge outline" style={{ fontSize: 9 }}>TODO</span>}
              </label>
            ))}
          </div>
          <div className="text-faint" style={{ fontSize: 11, marginTop: 10 }}>
            {t.tx('选择保存到 localStorage（nebula.admin.users.cols）。', 'Saved to localStorage (nebula.admin.users.cols).')}
          </div>
        </Modal>
      )}

      {/* Right-side drawer */}
      {drawerOpen && selected && (
        <UserDetail key={selected.id} user={selected} t={t} tab={tab} setTab={setTab} push={push}
                    onClose={closeDrawer}
                    isBlack={blackSet.has(selected.id)}
                    blacklistEntry={blacklist.find(b => b.value === selected.id)}
                    reloadBlacklist={reloadBlacklist} />
      )}
    </div>
  );
}

// ─────────────────────────────────────────────────────────────────────────────
// UserDetail — 右侧 80vw 抽屉 · 14 tab · 角色分视图
// ─────────────────────────────────────────────────────────────────────────────
function UserDetail({ user, t, tab, setTab, push, onClose, isBlack, blacklistEntry, reloadBlacklist }) {
  const [adjustOpen, setAdjustOpen] = React.useState(false);
  const [revealed, setRevealed] = React.useState(false);

  // Role view selector — defaults to session role, but admins may simulate.
  const sessionRole = _currentSessionRole();
  const [viewRole, setViewRole] = React.useState(sessionRole);
  // If session role isn't admin, lock the view (no switcher allowed).
  const canSwitchView = sessionRole === 'admin';

  const allowedTabs = TAB_ACCESS[viewRole] || TAB_ACCESS.admin;
  const tier = _tierFromRecharge(user.total_stake);
  const allTabs = _userTabs(t);
  const visibleTabs = allTabs.filter(([k]) => allowedTabs.includes(k));

  // If current tab is hidden under this role, fall back to overview.
  React.useEffect(() => {
    if (!allowedTabs.includes(tab)) setTab('overview');
  }, [allowedTabs, tab, setTab]);

  const can = (action) => {
    const allowed = ACTION_ACCESS[action] || [];
    return allowed.includes(viewRole);
  };

  const confirmTodo = (action, labelZh, labelEn) => {
    const msg = t.tx(
      `${labelZh}\n\n此操作走审批流（Phase 2 接入）。是否提交申请？`,
      `${labelEn}\n\nThis operation goes through approval (wired in Phase 2). Submit request?`
    );
    if (window.confirm(msg)) {
      _logTodo(action, user);
      push(t.tx(`已记录 ${labelZh} 申请（${user.id}）`, `Logged ${labelEn} request (${user.id})`));
    }
  };

  const revealContact = () => {
    if (viewRole !== 'admin') {
      push(t.tx('仅 admin 可查看完整联系方式', 'Only admin can reveal full contact'));
      return;
    }
    setRevealed(true);
    _logTodo('reveal_contact', user, 'audit-trail');
    push(t.tx('已记录敏感字段访问审计', 'Sensitive-field access logged to audit'));
  };

  // 13 quick actions
  const ACTIONS = [
    { k: 'freeze',          show: !isBlack, gate: 'freeze',        zh: '冻结账号', en: 'Freeze account',     cls: 'btn-danger', exec: () => addBlack() },
    { k: 'unfreeze',        show: isBlack,  gate: 'unfreeze',      zh: '解冻账号', en: 'Unfreeze account',   cls: '',           exec: () => removeBlack() },
    { k: 'limit_login',     show: true,     gate: 'limit_login',   zh: '限制登录', en: 'Limit login',        cls: '' },
    { k: 'limit_deposit',   show: true,     gate: 'limit_deposit', zh: '限制充值', en: 'Limit deposit',      cls: '' },
    { k: 'limit_withdraw',  show: true,     gate: 'limit_withdraw',zh: '限制提现', en: 'Limit withdraw',     cls: '' },
    { k: 'limit_bet',       show: true,     gate: 'limit_bet',     zh: '限制投注', en: 'Limit betting',      cls: '' },
    { k: 'force_logout',    show: true,     gate: 'force_logout',  zh: '强制下线', en: 'Force logout',       cls: '' },
    { k: 'reset_pw',        show: true,     gate: 'reset_pw',      zh: '重置密码', en: 'Reset password',     cls: '' },
    { k: 'change_tier',     show: true,     gate: 'change_tier',   zh: '修改会员等级', en: 'Change tier',    cls: '' },
    { k: 'add_tag',         show: true,     gate: 'add_tag',       zh: '添加标签', en: 'Add tag',            cls: '' },
    { k: 'adjust',          show: true,     gate: 'adjust',        zh: '提交调账申请', en: 'Adjust request', cls: '', exec: () => setAdjustOpen(true) },
    { k: 'freeze_funds',    show: true,     gate: 'freeze_funds',  zh: '提交冻结资金申请', en: 'Freeze funds request', cls: '' },
    { k: 'reveal',          show: true,     gate: 'reveal',        zh: '查看完整联系方式', en: 'Reveal contact', cls: '', exec: revealContact },
  ];

  // (defined below inside closure so addBlack/removeBlack can be hoisted)
  async function addBlack() {
    const reason = window.prompt(t.tx(`将 ${user.id} 加入黑名单的原因？`, `Reason for blacklisting ${user.id}?`), t.tx('管理员人工加黑', 'Manual blacklist by admin'));
    if (reason == null) return;
    try {
      await _adminFetch('/api/risk/blacklist', {
        method: 'POST',
        body: JSON.stringify({ kind: 'user', value: user.id, reason: reason || 'manual blacklist' }),
      });
      push(t.tx(`已加黑 ${user.id}`, `Blacklisted ${user.id}`));
      reloadBlacklist();
    } catch (e) {
      push(t.tx('加黑失败：', 'Blacklist failed: ') + ((e.body && e.body.error) || e.message));
    }
  }
  async function removeBlack() {
    if (!blacklistEntry) return;
    if (!window.confirm(t.tx(`将 ${user.id} 从黑名单移除？`, `Remove ${user.id} from blacklist?`))) return;
    try {
      await _adminFetch(`/api/risk/blacklist/${blacklistEntry.id}`, { method: 'DELETE' });
      push(t.tx(`已解除黑名单 ${user.id}`, `Unblacklisted ${user.id}`));
      reloadBlacklist();
    } catch (e) {
      push(t.tx('解除失败：', 'Unblacklist failed: ') + ((e.body && e.body.error) || e.message));
    }
  }

  const onlineToday = _isOnline(user) ? t.tx('在线', 'Online') : '—';
  const phoneShown = revealed ? (user.phone || '—') : _maskPhone(user.phone);
  const emailShown = revealed ? '—' : _maskEmail(null); // backend has no email yet

  return (
    <>
      <style>{`
        .user-drawer-backdrop { position: fixed; inset: 0; background: rgba(0,0,0,.45); z-index: 800; }
        .user-drawer {
          position: fixed; top: 0; right: 0; bottom: 0;
          width: 80vw; max-width: 1400px;
          background: var(--bg-card); z-index: 801;
          overflow-y: auto;
          box-shadow: -8px 0 32px rgba(0,0,0,.3);
          animation: user-drawer-slide-in 0.2s ease-out;
          display: flex; flex-direction: column;
        }
        @keyframes user-drawer-slide-in {
          from { transform: translateX(20px); opacity: 0 }
          to   { transform: translateX(0); opacity: 1 }
        }
        @media (max-width: 768px) {
          .user-drawer { width: 100vw; }
        }
        .user-drawer-info {
          position: sticky; top: 0; z-index: 5;
          background: var(--bg-card);
          border-bottom: 1px solid var(--border);
          padding: 16px 20px;
        }
        .user-info-grid {
          display: grid;
          grid-template-columns: repeat(6, 1fr);
          gap: 10px 16px;
          margin-top: 10px;
        }
        .user-info-cell { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
        .user-info-cell .lbl { font-size: 10.5px; color: var(--text-faint); }
        .user-info-cell .val { font-size: 12.5px; color: var(--text); font-variant-numeric: tabular-nums; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
        .user-quick-actions { display: flex; flex-wrap: wrap; gap: 6px; }
        @media (max-width: 900px) {
          .user-info-grid { grid-template-columns: repeat(3, 1fr); }
        }
      `}</style>
      <div className="user-drawer-backdrop" onClick={onClose} />
      <div className="user-drawer" role="dialog" aria-label="user-detail">
        {/* Sticky info card */}
        <div className="user-drawer-info">
          <div style={{ display: 'flex', alignItems: 'flex-start', gap: 14 }}>
            <Avatar name={user.name || user.id} size={56} />
            <div style={{ flex: 1, minWidth: 0 }}>
              <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 4 }}>
                <h2 style={{ margin: 0, fontSize: 17, fontWeight: 600 }}>{user.name || t.tx('(未命名)', '(unnamed)')}</h2>
                <TierBadge tier={tier} t={t} />
                {isBlack
                  ? <span className="badge danger">{t.tx('冻结', 'Frozen')}</span>
                  : <span className="badge success">{t.tx('正常', 'Normal')}</span>}
                {user.role && user.role !== 'user' && <span className="badge outline">{user.role}</span>}
                {_isOnline(user) && <span className="badge success" style={{ fontSize: 10 }}>{t.tx('在线', 'Online')}</span>}
              </div>
              <div style={{ display: 'flex', gap: 14, fontSize: 11.5, color: 'var(--text-muted)', flexWrap: 'wrap' }}>
                <span className="text-mono">{user.id}</span>
                <span>{phoneShown}</span>
                <span data-todo="endpoint:/api/admin/users/{id}/profile">{emailShown}</span>
              </div>
            </div>
            <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-end', gap: 8 }}>
              {canSwitchView ? (
                <select className="select" style={{ width: 140 }} value={viewRole} onChange={(e) => setViewRole(e.target.value)}>
                  <option value="admin">{t.tx('视图: admin', 'View: admin')}</option>
                  <option value="finance">{t.tx('视图: finance', 'View: finance')}</option>
                  <option value="ops">{t.tx('视图: ops', 'View: ops')}</option>
                  <option value="support">{t.tx('视图: support', 'View: support')}</option>
                  <option value="risk">{t.tx('视图: risk', 'View: risk')}</option>
                </select>
              ) : (
                <span className="badge outline" style={{ fontSize: 10 }}>{t.tx('视图', 'View')}: {viewRole}</span>
              )}
              <button className="btn btn-sm btn-ghost" onClick={onClose}><Icons.x /> {t.tx('关闭', 'Close')}</button>
            </div>
          </div>

          {/* 18-field grid (3 rows × 6 cells) */}
          <div className="user-info-grid">
            <div className="user-info-cell"><span className="lbl">{t.tx('会员ID', 'Member ID')}</span><span className="val text-mono">{user.id}</span></div>
            <div className="user-info-cell"><span className="lbl">{t.tx('账号', 'Account')}</span><span className="val" data-todo="endpoint:/api/admin/users/{id}/profile">—</span></div>
            <div className="user-info-cell"><span className="lbl">{t.tx('昵称', 'Nickname')}</span><span className="val">{user.name || '—'}</span></div>
            <div className="user-info-cell"><span className="lbl">{t.tx('状态', 'Status')}</span><span className="val">{isBlack ? <span className="badge danger" style={{ fontSize: 10 }}>{t.tx('冻结', 'Frozen')}</span> : <span className="badge success" style={{ fontSize: 10 }}>{t.tx('正常', 'Normal')}</span>}</span></div>
            <div className="user-info-cell"><span className="lbl">{t.tx('会员等级', 'Tier')}</span><span className="val"><TierBadge tier={tier} t={t} /></span></div>
            <div className="user-info-cell"><span className="lbl">{t.tx('VIP等级', 'VIP')}</span><span className="val" data-todo="endpoint:/api/admin/users/{id}/vip">—</span></div>

            <div className="user-info-cell"><span className="lbl">{t.tx('所属租户', 'Tenant')}</span><span className="val" data-todo="endpoint:/api/admin/tenants">nebula</span></div>
            <div className="user-info-cell"><span className="lbl">{t.tx('所属站点', 'Site')}</span><span className="val" data-todo="endpoint:/api/admin/sites">main</span></div>
            <div className="user-info-cell"><span className="lbl">{t.tx('注册时间', 'Registered')}</span><span className="val">{(user.created_at || '').slice(0, 19).replace('T', ' ') || '—'}</span></div>
            <div className="user-info-cell"><span className="lbl">{t.tx('最近登录', 'Last login')}</span><span className="val">{user.last_bet ? _relTime(user.last_bet) : '—'}</span></div>
            <div className="user-info-cell"><span className="lbl">{t.tx('今日在线时长', 'Online today')}</span><span className="val" data-todo="endpoint:/api/admin/users/{id}/online-stats">{onlineToday}</span></div>
            <div className="user-info-cell"><span className="lbl">{t.tx('累计在线时长', 'Total online')}</span><span className="val" data-todo="endpoint:/api/admin/users/{id}/online-stats">—</span></div>

            <div className="user-info-cell"><span className="lbl">{t.tx('余额', 'Balance')}</span><span className="val">{fmtCN(user.balance || 0)}</span></div>
            <div className="user-info-cell"><span className="lbl">{t.tx('冻结金额', 'Frozen amount')}</span><span className="val" data-todo="endpoint:/api/admin/users/{id}/wallet">—</span></div>
            <div className="user-info-cell"><span className="lbl">{t.tx('可提现余额', 'Cashable')}</span><span className="val" data-todo="endpoint:/api/admin/users/{id}/wallet">—</span></div>
            <div className="user-info-cell"><span className="lbl">{t.tx('累计充值', 'Total deposit')}</span><span className="val" data-todo="endpoint:/api/admin/users/{id}/wallet">—</span></div>
            <div className="user-info-cell"><span className="lbl">{t.tx('累计提现', 'Total withdraw')}</span><span className="val" data-todo="endpoint:/api/admin/users/{id}/wallet">—</span></div>
            <div className="user-info-cell">
              <span className="lbl">{t.tx('用户标签', 'User tags')}</span>
              <span className="val" data-todo="endpoint:/api/admin/users/{id}/tags" style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
                {isBlack && <span className="badge danger" style={{ fontSize: 9 }}>blacklist</span>}
                {(user.bets_count || 0) > 100 && <span className="badge info" style={{ fontSize: 9 }}>active</span>}
                {(user.balance || 0) > 100000 && <span className="badge gold" style={{ fontSize: 9 }}>high-balance</span>}
                {(!isBlack && (user.bets_count || 0) <= 100 && (user.balance || 0) <= 100000) && <span className="text-faint" style={{ fontSize: 11 }}>—</span>}
              </span>
            </div>
          </div>

          {/* 13 quick-action buttons */}
          <div style={{ marginTop: 12, paddingTop: 10, borderTop: '1px dashed var(--border)' }}>
            <div className="user-quick-actions">
              {ACTIONS.filter(a => a.show && can(a.gate)).map(a => (
                <button key={a.k}
                        className={'btn btn-xs ' + (a.cls || '')}
                        onClick={() => {
                          if (a.exec) a.exec();
                          else confirmTodo(a.k, a.zh, a.en);
                        }}>
                  {t.tx(a.zh, a.en)}
                </button>
              ))}
              {revealed && (
                <span className="badge warning" style={{ fontSize: 10 }}>
                  {t.tx('已揭露 · ', 'Revealed · ')}{user.phone || '—'}
                </span>
              )}
            </div>
          </div>

          {/* 14-tab strip (filtered by role) */}
          <div className="tabs" style={{ marginTop: 14, marginBottom: 0 }}>
            {visibleTabs.map(([k, l]) => (
              <div key={k} className={'tab' + (tab === k ? ' active' : '')} onClick={() => setTab(k)}>{l}</div>
            ))}
          </div>
        </div>

        {/* Tab body */}
        <div style={{ padding: 20, flex: 1 }}>
          {tab === 'overview' && <UserOverview user={user} t={t} push={push} onAdjust={() => setAdjustOpen(true)} />}
          {tab === 'profile'  && <UserProfileTab user={user} t={t} push={push} revealed={revealed} />}
          {tab === 'devices'  && <UserDevicesTab user={user} t={t} push={push} />}
          {tab === 'tier'     && <UserTierTab user={user} t={t} push={push} />}
          {tab === 'wallet'   && <UserWalletTab user={user} t={t} push={push} />}
          {tab === 'orders'   && <UserOrdersTab user={user} t={t} push={push} />}
          {tab === 'promo'    && <UserPromoTab user={user} t={t} push={push} />}
          {tab === 'content'  && <UserContentTab user={user} t={t} push={push} />}
          {tab === 'live'     && <UserLiveTab user={user} t={t} push={push} />}
          {tab === 'bets'     && <UserBetsTab user={user} t={t} push={push} />}
          {tab === 'referral' && <UserReferralTab user={user} t={t} push={push} />}
          {tab === 'risk'     && <UserRiskTab user={user} t={t} push={push} />}
          {tab === 'support'  && <UserSupportTab user={user} t={t} push={push} />}
          {tab === 'audit'    && <UserAuditTab user={user} t={t} push={push} />}
        </div>

        {adjustOpen && <WalletAdjustModal user={user} t={t} onClose={() => setAdjustOpen(false)} push={push} />}
      </div>
    </>
  );
}

// ─── Tab 1: 概览（基本信息 + 风险评分） ──────────────────────────────────────
function UserOverview({ user, t, push, onAdjust }) {
  const [risk, setRisk] = React.useState(null);
  const [riskLoading, setRiskLoading] = React.useState(false);
  const [riskErr, setRiskErr] = React.useState('');

  const checkRisk = React.useCallback(async () => {
    setRiskLoading(true);
    setRiskErr('');
    try {
      const d = await _adminFetch('/api/risk/check', {
        method: 'POST',
        body: JSON.stringify({ user_id: user.id, action: 'withdraw', amount: 100 }),
      });
      setRisk(d);
    } catch (e) {
      setRiskErr((e.body && e.body.error) || e.message);
    } finally {
      setRiskLoading(false);
    }
  }, [user.id]);

  React.useEffect(() => { checkRisk(); }, [checkRisk]);

  const tier = _tierFromRecharge(user.total_stake);

  return (
    <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16 }}>
      <div className="card">
        <div className="card-h"><h3>{t.tx('基础信息', 'Basic info')}</h3></div>
        <div style={{ padding: 16 }}>
          <dl className="kv">
            <dt>{t.tx('用户 ID', 'User ID')}</dt><dd className="text-mono">{user.id}</dd>
            <dt>{t.tx('名称', 'Name')}</dt><dd>{user.name || '—'}</dd>
            <dt>{t.tx('手机号', 'Phone')}</dt><dd className="text-mono">{user.phone || '—'}</dd>
            <dt>{t.tx('角色', 'Role')}</dt><dd><span className="badge outline">{user.role || 'user'}</span></dd>
            <dt>{t.tx('注册时间', 'Registered')}</dt><dd>{(user.created_at || '').slice(0, 19).replace('T', ' ') || '—'}</dd>
            <dt>{t.tx('钱包余额', 'Wallet balance')}</dt><dd className="tabular">{fmtCN(user.balance || 0)} <button className="btn btn-xs" style={{ marginLeft: 8 }} onClick={onAdjust}>{t.tx('调整', 'Adjust')}</button></dd>
            <dt>{t.tx('金币', 'Coins')}</dt><dd className="tabular">{(user.coins || 0).toLocaleString()}</dd>
            <dt>{t.tx('累计流水', 'Total stake')}</dt><dd className="tabular">{fmtCN(user.total_stake || 0)}</dd>
            <dt>{t.tx('投注笔数', 'Bet count')}</dt><dd className="tabular">{(user.bets_count || 0).toLocaleString()}</dd>
            <dt>{t.tx('最近投注', 'Last bet')}</dt><dd>{(user.last_bet || '').slice(0, 19).replace('T', ' ') || '—'}</dd>
            <dt>{t.tx('派生等级', 'Derived tier')}</dt><dd><TierBadge tier={tier} t={t} /></dd>
          </dl>
        </div>
      </div>
      <div className="card">
        <div className="card-h">
          <h3>{t.tx('实时风险评分', 'Live risk score')}</h3>
          <span className="meta">{t.tx('withdraw ¥100 假设动作', 'withdraw ¥100 hypothesis')}</span>
        </div>
        <div style={{ padding: 16 }}>
          {riskLoading && <div className="text-muted">{t.tx('查询中…', 'Querying…')}</div>}
          {riskErr && <div style={{ color: 'var(--danger)', fontSize: 12 }}>{riskErr}</div>}
          {risk && (
            <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
              <div style={{ display: 'flex', alignItems: 'center', gap: 14 }}>
                <RiskDot score={risk.score || 0} />
                <div style={{ fontSize: 11.5 }}>
                  {t.tx('决策', 'Decision')} <span className="badge outline" style={{
                    color: risk.decision === 'deny' ? 'var(--danger)' :
                           risk.decision === 'review' ? 'var(--warning)' : 'var(--success)',
                  }}>{risk.decision || '—'}</span>
                </div>
                <button className="btn btn-xs btn-ghost" onClick={checkRisk}>{t.tx('重新评估', 'Re-evaluate')}</button>
              </div>
              <div>
                <div className="text-muted" style={{ fontSize: 11, marginBottom: 4 }}>{t.tx('命中规则', 'Matched rules')} ({(risk.reasons || []).length})</div>
                {(risk.reasons || []).length === 0 && <div className="text-faint" style={{ fontSize: 11 }}>{t.tx('无', 'None')}</div>}
                <div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
                  {(risk.reasons || []).map((r, i) => (
                    <div key={i} style={{ fontSize: 11.5, display: 'flex', gap: 6 }}>
                      <span className="badge outline" style={{ fontSize: 10 }}>{r.code || r.rule || '?'}</span>
                      <span className="text-muted">{r.message || r.detail || JSON.stringify(r)}</span>
                    </div>
                  ))}
                </div>
              </div>
            </div>
          )}
          {!risk && !riskLoading && !riskErr && <div className="text-faint">{t.tx('点击重新评估查询', 'Click Re-evaluate to query')}</div>}
        </div>
      </div>
    </div>
  );
}

// ─── Tab 2: 基础资料（placeholder） ─────────────────────────────────────────
function UserProfileTab({ user, t, push, revealed }) {
  return (
    <div className="stack">
      <h3>{t.tx('基础资料', 'Profile')}</h3>
      <div className="card" style={{ padding: 16 }}>
        <h4 style={{ marginTop: 0 }}>{t.tx('实名信息', 'KYC info')}</h4>
        <dl className="kv">
          <dt>{t.tx('真实姓名', 'Real name')}</dt><dd data-todo="endpoint:/api/admin/users/{id}/profile">—</dd>
          <dt>{t.tx('证件类型', 'ID type')}</dt><dd data-todo="endpoint:/api/admin/users/{id}/kyc">—</dd>
          <dt>{t.tx('证件号', 'ID number')}</dt><dd data-todo="endpoint:/api/admin/users/{id}/kyc">—</dd>
          <dt>{t.tx('出生日期', 'Birthday')}</dt><dd data-todo="endpoint:/api/admin/users/{id}/profile">—</dd>
          <dt>{t.tx('国籍/地区', 'Country/Region')}</dt><dd data-todo="endpoint:/api/admin/users/{id}/profile">—</dd>
        </dl>
      </div>
      <div className="card" style={{ padding: 16 }}>
        <h4 style={{ marginTop: 0 }}>{t.tx('联系方式', 'Contact')}</h4>
        <dl className="kv">
          <dt>{t.tx('手机号', 'Phone')}</dt><dd>{revealed ? (user.phone || '—') : _maskPhone(user.phone)}</dd>
          <dt>{t.tx('邮箱', 'Email')}</dt><dd data-todo="endpoint:/api/admin/users/{id}/profile">—</dd>
          <dt>{t.tx('Telegram', 'Telegram')}</dt><dd data-todo="endpoint:/api/admin/users/{id}/profile">—</dd>
          <dt>{t.tx('QQ/微信', 'QQ/WeChat')}</dt><dd data-todo="endpoint:/api/admin/users/{id}/profile">—</dd>
        </dl>
      </div>
      <div className="card" style={{ padding: 16 }}>
        <h4 style={{ marginTop: 0 }}>{t.tx('注册信息', 'Registration')}</h4>
        <dl className="kv">
          <dt>{t.tx('注册时间', 'Registered')}</dt><dd>{(user.created_at || '').slice(0, 19).replace('T', ' ') || '—'}</dd>
          <dt>{t.tx('注册渠道', 'Channel')}</dt><dd data-todo="endpoint:/api/admin/users/{id}/profile">—</dd>
          <dt>{t.tx('注册 IP', 'Register IP')}</dt><dd data-todo="endpoint:/api/admin/users/{id}/profile">—</dd>
          <dt>{t.tx('注册设备', 'Register device')}</dt><dd data-todo="endpoint:/api/admin/users/{id}/profile">—</dd>
          <dt>{t.tx('邀请人', 'Inviter')}</dt><dd data-todo="endpoint:/api/admin/users/{id}/profile">—</dd>
        </dl>
      </div>
      <div className="text-faint" style={{ fontSize: 11 }}>{t.tx('Phase 2 接入：profile / KYC 字段。', 'Phase 2: profile / KYC fields.')}</div>
    </div>
  );
}

// ─── Tab 3: 登录与设备 ──────────────────────────────────────────────────────
function UserDevicesTab({ user, t, push }) {
  const [devList] = _useAdminPoll('/api/admin/devices?limit=200', 15000, true);
  const [reason, setReason] = React.useState('');
  const [busyId, setBusyId] = React.useState(null);

  const allDevices = (devList && devList.devices) || [];
  const userDevices = allDevices.filter(d => {
    if (d.last_user_id === user.id) return true;
    if (Array.isArray(d.accounts) && d.accounts.includes(user.id)) return true;
    return false;
  });

  const banDevice = async (did) => {
    if (!window.confirm(t.tx(`封禁设备 ${_shortId(did, 12)}?`, `Ban device ${_shortId(did, 12)}?`))) return;
    setBusyId(did);
    try {
      await _adminFetch(`/api/admin/devices/${did}/ban`, {
        method: 'POST',
        body: JSON.stringify({ reason: reason || `risk ban from ${user.id}` }),
      });
      push(t.tx('已封禁 ', 'Banned ') + _shortId(did, 10));
    } catch (e) {
      push(t.tx('封禁失败：', 'Ban failed: ') + e.message);
    } finally {
      setBusyId(null);
    }
  };

  const clearDevice = async (did) => {
    setBusyId(did);
    try {
      await _adminFetch(`/api/admin/devices/${did}/clear`, { method: 'POST' });
      push(t.tx('已清除标记 ', 'Cleared flag ') + _shortId(did, 10));
    } catch (e) {
      push(t.tx('清除失败：', 'Clear failed: ') + e.message);
    } finally {
      setBusyId(null);
    }
  };

  return (
    <div className="card">
      <div style={{ padding: 12, display: 'flex', alignItems: 'center', gap: 8, borderBottom: '1px solid var(--border)' }}>
        <input className="input" placeholder={t.tx('封禁理由（可选）', 'Ban reason (optional)')} style={{ flex: 1 }} value={reason} onChange={(e) => setReason(e.target.value)} />
        <span className="text-faint" style={{ fontSize: 11 }}>{userDevices.length} / {allDevices.length} {t.tx('个设备（filter by user_id）', 'devices (filter by user_id)')}</span>
      </div>
      <div className="tbl-scroll">
      <table className="tbl tbl-stack">
        <thead><tr>
          <th>device_id</th><th>flag</th><th>{t.tx('风险', 'Risk')}</th><th>{t.tx('最近 IP', 'Last IP')}</th><th>UA</th><th>last_seen</th><th></th>
        </tr></thead>
        <tbody>
          {!devList && <tr><td colSpan={7} style={{ textAlign: 'center', padding: 24, color: 'var(--text-muted)' }}>{t.tx('加载中…', 'Loading…')}</td></tr>}
          {devList && userDevices.length === 0 && <tr><td colSpan={7} style={{ textAlign: 'center', padding: 24, color: 'var(--text-muted)' }}>{t.tx('未发现该用户登录过的设备', 'No devices found for this user')}</td></tr>}
          {userDevices.map(d => (
            <tr key={d.device_id}>
              <td data-label="device_id" className="text-mono" style={{ fontSize: 11 }}>{_shortId(d.device_id, 18)}</td>
              <td data-label="flag">
                <span className={'badge ' + (d.flag === 'banned' ? 'danger' : d.flag === 'flagged' || d.flag === 'shadow' ? 'warning' : 'outline')}>
                  {d.flag || 'normal'}
                </span>
              </td>
              <td data-label={t.tx('风险', 'Risk')}><RiskDot score={d.risk_score || 0} /></td>
              <td data-label={t.tx('最近 IP', 'Last IP')} className="text-mono" style={{ fontSize: 11 }}>{d.last_ip || '—'}</td>
              <td data-label="UA" className="text-faint" style={{ fontSize: 10.5, maxWidth: 200, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{d.user_agent || '—'}</td>
              <td data-label="last_seen" className="muted" style={{ fontSize: 11 }}>{_relTime(d.last_seen_at)}</td>
              <td data-label={t.tx('操作', 'Actions')} className="tbl-actions">
                <button className="btn btn-xs btn-ghost" disabled={busyId === d.device_id} onClick={() => clearDevice(d.device_id)}>{t.tx('清除', 'Clear')}</button>
                <button className="btn btn-xs btn-danger" disabled={busyId === d.device_id} onClick={() => banDevice(d.device_id)}>{t.tx('封禁', 'Ban')}</button>
              </td>
            </tr>
          ))}
        </tbody>
      </table>
      </div>
    </div>
  );
}

// ─── Tab 4: 会员等级（placeholder） ─────────────────────────────────────────
function UserTierTab({ user, t, push }) {
  return (
    <div className="stack">
      <h3>{t.tx('会员等级', 'Tier')}</h3>
      <div className="card" style={{ padding: 16 }}>
        <h4 style={{ marginTop: 0 }}>{t.tx('当前等级', 'Current tier')}</h4>
        <dl className="kv">
          <dt>{t.tx('普通等级', 'Tier')}</dt><dd><TierBadge tier={_tierFromRecharge(user.total_stake)} t={t} /></dd>
          <dt>{t.tx('VIP 等级', 'VIP level')}</dt><dd data-todo="endpoint:/api/admin/users/{id}/vip">—</dd>
          <dt>{t.tx('成长值', 'Growth points')}</dt><dd data-todo="endpoint:/api/admin/users/{id}/vip">—</dd>
          <dt>{t.tx('下一级所需', 'To next level')}</dt><dd data-todo="endpoint:/api/admin/users/{id}/vip">—</dd>
        </dl>
      </div>
      <div className="card" style={{ padding: 16 }}>
        <h4 style={{ marginTop: 0 }}>{t.tx('等级权益', 'Tier perks')}</h4>
        <div className="text-faint" style={{ fontSize: 11 }} data-todo="endpoint:/api/admin/users/{id}/vip/perks">—</div>
      </div>
      <div className="card" style={{ padding: 16 }}>
        <h4 style={{ marginTop: 0 }}>{t.tx('升级历史', 'Tier history')}</h4>
        <div className="text-faint" style={{ fontSize: 11 }} data-todo="endpoint:/api/admin/users/{id}/vip/history">—</div>
      </div>
      <div className="text-faint" style={{ fontSize: 11 }}>{t.tx('Phase 2 接入：VIP 等级、成长值、权益、升降级历史。', 'Phase 2: VIP level, growth points, perks, level history.')}</div>
    </div>
  );
}

// ─── Tab 5: 钱包与账本（placeholder） ───────────────────────────────────────
function UserWalletTab({ user, t, push }) {
  return (
    <div className="stack">
      <h3>{t.tx('钱包与账本', 'Wallet & Ledger')}</h3>
      <div className="card" style={{ padding: 16 }}>
        <h4 style={{ marginTop: 0 }}>{t.tx('钱包概览', 'Wallet overview')}</h4>
        <dl className="kv">
          <dt>{t.tx('现金余额', 'Cash balance')}</dt><dd>{fmtCN(user.balance || 0)}</dd>
          <dt>{t.tx('冻结余额', 'Frozen')}</dt><dd data-todo="endpoint:/api/admin/users/{id}/wallet">—</dd>
          <dt>{t.tx('可提现余额', 'Cashable')}</dt><dd data-todo="endpoint:/api/admin/users/{id}/wallet">—</dd>
          <dt>{t.tx('赠金余额', 'Bonus')}</dt><dd data-todo="endpoint:/api/admin/users/{id}/wallet">—</dd>
          <dt>{t.tx('累计充值', 'Total deposit')}</dt><dd data-todo="endpoint:/api/admin/users/{id}/wallet">—</dd>
          <dt>{t.tx('累计提现', 'Total withdraw')}</dt><dd data-todo="endpoint:/api/admin/users/{id}/wallet">—</dd>
          <dt>{t.tx('累计人工加款', 'Total manual credit')}</dt><dd data-todo="endpoint:/api/audit?action=adjust&target={id}">—</dd>
        </dl>
      </div>
      <div className="card" style={{ padding: 16 }}>
        <h4 style={{ marginTop: 0 }}>{t.tx('账本流水', 'Ledger')}</h4>
        <div className="text-faint" style={{ fontSize: 11 }} data-todo="endpoint:/api/admin/users/{id}/ledger">{t.tx('Phase 2 接入：按 user_id 查 ledger 表', 'Phase 2: query ledger by user_id')}</div>
      </div>
      <div className="card" style={{ padding: 16 }}>
        <h4 style={{ marginTop: 0 }}>{t.tx('资金操作', 'Money ops')}</h4>
        <div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
          <button className="btn btn-sm">{t.tx('人工加款申请', 'Credit request')}</button>
          <button className="btn btn-sm">{t.tx('人工扣款申请', 'Debit request')}</button>
          <button className="btn btn-sm">{t.tx('冻结金额申请', 'Freeze request')}</button>
          <button className="btn btn-sm">{t.tx('解冻金额申请', 'Unfreeze request')}</button>
          <button className="btn btn-sm">{t.tx('冲正申请', 'Reversal request')}</button>
        </div>
      </div>
    </div>
  );
}

// ─── Tab 6: 充值/提现订单 ───────────────────────────────────────────────────
function UserOrdersTab({ user, t, push }) {
  return (
    <div className="stack">
      <h3>{t.tx('充值/提现订单', 'Deposit/Withdraw orders')}</h3>
      <div className="card" style={{ padding: 16 }}>
        <h4 style={{ marginTop: 0 }}>{t.tx('充值记录', 'Deposit records')}</h4>
        <div className="text-faint" style={{ fontSize: 11 }} data-todo="endpoint:/api/admin/deposits?user_id={id}">—</div>
      </div>
      <div className="card" style={{ padding: 16 }}>
        <h4 style={{ marginTop: 0 }}>{t.tx('提现记录', 'Withdraw records')}</h4>
        <div className="text-faint" style={{ fontSize: 11 }} data-todo="endpoint:/api/admin/withdraws?user_id={id}">—</div>
      </div>
      <div className="card" style={{ padding: 16 }}>
        <h4 style={{ marginTop: 0 }}>{t.tx('支付通道使用', 'Payment-channel usage')}</h4>
        <div className="text-faint" style={{ fontSize: 11 }} data-todo="endpoint:/api/admin/users/{id}/channels">—</div>
      </div>
      <div className="card" style={{ padding: 16 }}>
        <h4 style={{ marginTop: 0 }}>{t.tx('收款账户', 'Receiving accounts')}</h4>
        <div className="text-faint" style={{ fontSize: 11 }} data-todo="endpoint:/api/admin/users/{id}/payout-accounts">—</div>
      </div>
    </div>
  );
}

// ─── Tab 7: 活动/任务 ───────────────────────────────────────────────────────
function UserPromoTab({ user, t, push }) {
  return (
    <div className="stack">
      <h3>{t.tx('活动/任务', 'Promotions/Tasks')}</h3>
      <div className="card" style={{ padding: 16 }}>
        <h4 style={{ marginTop: 0 }}>{t.tx('参与的活动', 'Joined promotions')}</h4>
        <div className="text-faint" style={{ fontSize: 11 }} data-todo="endpoint:/api/admin/promo/participants?user_id={id}">—</div>
      </div>
      <div className="card" style={{ padding: 16 }}>
        <h4 style={{ marginTop: 0 }}>{t.tx('领取的奖金/红包', 'Bonus / red packets')}</h4>
        <div className="text-faint" style={{ fontSize: 11 }} data-todo="endpoint:/api/admin/users/{id}/bonus">—</div>
      </div>
      <div className="card" style={{ padding: 16 }}>
        <h4 style={{ marginTop: 0 }}>{t.tx('签到/任务进度', 'Check-in / task progress')}</h4>
        <div className="text-faint" style={{ fontSize: 11 }} data-todo="endpoint:/api/admin/users/{id}/tasks">—</div>
      </div>
      <div className="card" style={{ padding: 16 }}>
        <h4 style={{ marginTop: 0 }}>{t.tx('使用的优惠码', 'Used promo codes')}</h4>
        <div className="text-faint" style={{ fontSize: 11 }} data-todo="endpoint:/api/admin/users/{id}/coupons">—</div>
      </div>
    </div>
  );
}

// ─── Tab 8: 内容/视频 ───────────────────────────────────────────────────────
function UserContentTab({ user, t, push }) {
  return (
    <div className="stack">
      <h3>{t.tx('内容/视频', 'Content & Video')}</h3>
      <div className="card" style={{ padding: 16 }}>
        <h4 style={{ marginTop: 0 }}>{t.tx('观看历史', 'Watch history')}</h4>
        <div className="text-faint" style={{ fontSize: 11 }} data-todo="endpoint:/api/admin/users/{id}/watch-history">—</div>
      </div>
      <div className="card" style={{ padding: 16 }}>
        <h4 style={{ marginTop: 0 }}>{t.tx('收藏/点赞', 'Favorites / likes')}</h4>
        <div className="text-faint" style={{ fontSize: 11 }} data-todo="endpoint:/api/admin/users/{id}/likes">—</div>
      </div>
      <div className="card" style={{ padding: 16 }}>
        <h4 style={{ marginTop: 0 }}>{t.tx('解锁记录', 'Unlock records')}</h4>
        <div className="text-faint" style={{ fontSize: 11 }} data-todo="endpoint:/api/admin/users/{id}/unlocks">—</div>
      </div>
      <div className="card" style={{ padding: 16 }}>
        <h4 style={{ marginTop: 0 }}>{t.tx('内容消费时长', 'Content viewing time')}</h4>
        <div className="text-faint" style={{ fontSize: 11 }} data-todo="endpoint:/api/admin/users/{id}/content-stats">—</div>
      </div>
    </div>
  );
}

// ─── Tab 9: 直播行为 ────────────────────────────────────────────────────────
function UserLiveTab({ user, t, push }) {
  return (
    <div className="stack">
      <h3>{t.tx('直播行为', 'Live behavior')}</h3>
      <div className="card" style={{ padding: 16 }}>
        <h4 style={{ marginTop: 0 }}>{t.tx('观看直播记录', 'Live watch records')}</h4>
        <div className="text-faint" style={{ fontSize: 11 }} data-todo="endpoint:/api/admin/users/{id}/live-watch">—</div>
      </div>
      <div className="card" style={{ padding: 16 }}>
        <h4 style={{ marginTop: 0 }}>{t.tx('打赏记录', 'Gifts sent')}</h4>
        <div className="text-faint" style={{ fontSize: 11 }} data-todo="endpoint:/api/admin/users/{id}/gifts">—</div>
      </div>
      <div className="card" style={{ padding: 16 }}>
        <h4 style={{ marginTop: 0 }}>{t.tx('关注的主播', 'Followed streamers')}</h4>
        <div className="text-faint" style={{ fontSize: 11 }} data-todo="endpoint:/api/admin/users/{id}/follows">—</div>
      </div>
      <div className="card" style={{ padding: 16 }}>
        <h4 style={{ marginTop: 0 }}>{t.tx('弹幕/聊天记录', 'Chat logs')}</h4>
        <div className="text-faint" style={{ fontSize: 11 }} data-todo="endpoint:/api/admin/users/{id}/chat-logs">—</div>
      </div>
    </div>
  );
}

// ─── Tab 10: 游戏/投注 ──────────────────────────────────────────────────────
function UserBetsTab({ user, t, push }) {
  return (
    <div className="stack">
      <h3>{t.tx('游戏/投注', 'Games & Bets')}</h3>
      <div className="card" style={{ padding: 16 }}>
        <h4 style={{ marginTop: 0 }}>{t.tx('投注汇总', 'Bet summary')}</h4>
        <dl className="kv">
          <dt>{t.tx('累计投注笔数', 'Bet count')}</dt><dd>{(user.bets_count || 0).toLocaleString()}</dd>
          <dt>{t.tx('累计流水', 'Total stake')}</dt><dd>{fmtCN(user.total_stake || 0)}</dd>
          <dt>{t.tx('最近投注', 'Last bet')}</dt><dd>{user.last_bet ? _relTime(user.last_bet) : '—'}</dd>
          <dt>{t.tx('累计盈亏', 'Total PnL')}</dt><dd data-todo="endpoint:/api/admin/users/{id}/bets-stats">—</dd>
        </dl>
      </div>
      <div className="card" style={{ padding: 16 }}>
        <h4 style={{ marginTop: 0 }}>{t.tx('投注明细', 'Bet records')}</h4>
        <div className="text-faint" style={{ fontSize: 11 }} data-todo="endpoint:/api/admin/bets?user_id={id}">—</div>
      </div>
      <div className="card" style={{ padding: 16 }}>
        <h4 style={{ marginTop: 0 }}>{t.tx('偏好的游戏', 'Preferred games')}</h4>
        <div className="text-faint" style={{ fontSize: 11 }} data-todo="endpoint:/api/admin/users/{id}/games-pref">—</div>
      </div>
      <div className="card" style={{ padding: 16 }}>
        <h4 style={{ marginTop: 0 }}>{t.tx('开奖/结算异常', 'Settlement anomalies')}</h4>
        <div className="text-faint" style={{ fontSize: 11 }} data-todo="endpoint:/api/admin/users/{id}/settle-issues">—</div>
      </div>
    </div>
  );
}

// ─── Tab 11: 代理/邀请 ──────────────────────────────────────────────────────
function UserReferralTab({ user, t, push }) {
  return (
    <div className="stack">
      <h3>{t.tx('代理/邀请', 'Referral')}</h3>
      <div className="card" style={{ padding: 16 }}>
        <h4 style={{ marginTop: 0 }}>{t.tx('代理身份', 'Agent identity')}</h4>
        <dl className="kv">
          <dt>{t.tx('是否代理', 'Is agent')}</dt><dd data-todo="endpoint:/api/admin/users/{id}/agent">—</dd>
          <dt>{t.tx('代理层级', 'Agent level')}</dt><dd data-todo="endpoint:/api/admin/users/{id}/agent">—</dd>
          <dt>{t.tx('分成比例', 'Commission rate')}</dt><dd data-todo="endpoint:/api/admin/users/{id}/agent">—</dd>
        </dl>
      </div>
      <div className="card" style={{ padding: 16 }}>
        <h4 style={{ marginTop: 0 }}>{t.tx('邀请人/上级', 'Inviter / upline')}</h4>
        <div className="text-faint" style={{ fontSize: 11 }} data-todo="endpoint:/api/admin/users/{id}/upline">—</div>
      </div>
      <div className="card" style={{ padding: 16 }}>
        <h4 style={{ marginTop: 0 }}>{t.tx('下级团队', 'Downline team')}</h4>
        <div className="text-faint" style={{ fontSize: 11 }} data-todo="endpoint:/api/admin/users/{id}/downline">—</div>
      </div>
      <div className="card" style={{ padding: 16 }}>
        <h4 style={{ marginTop: 0 }}>{t.tx('佣金记录', 'Commission records')}</h4>
        <div className="text-faint" style={{ fontSize: 11 }} data-todo="endpoint:/api/admin/users/{id}/commissions">—</div>
      </div>
    </div>
  );
}

// ─── Tab 12: 风控记录 ──────────────────────────────────────────────────────
function UserRiskTab({ user, t, push }) {
  const [risk, setRisk] = React.useState(null);
  const [riskErr, setRiskErr] = React.useState('');
  React.useEffect(() => {
    let cancelled = false;
    (async () => {
      try {
        const d = await _adminFetch('/api/risk/check', {
          method: 'POST',
          body: JSON.stringify({ user_id: user.id, action: 'withdraw', amount: 100 }),
        });
        if (!cancelled) setRisk(d);
      } catch (e) {
        if (!cancelled) setRiskErr((e.body && e.body.error) || e.message);
      }
    })();
    return () => { cancelled = true; };
  }, [user.id]);

  return (
    <div className="stack">
      <h3>{t.tx('风控记录', 'Risk records')}</h3>
      <div className="card" style={{ padding: 16 }}>
        <h4 style={{ marginTop: 0 }}>{t.tx('当前风险评分', 'Current risk score')}</h4>
        {riskErr && <div style={{ color: 'var(--danger)', fontSize: 12 }}>{riskErr}</div>}
        {risk ? (
          <div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
            <RiskDot score={risk.score || 0} />
            <span className="badge outline" style={{ color: risk.decision === 'deny' ? 'var(--danger)' : risk.decision === 'review' ? 'var(--warning)' : 'var(--success)' }}>{risk.decision || '—'}</span>
            <span className="text-faint" style={{ fontSize: 11 }}>{(risk.reasons || []).length} {t.tx('条命中', 'matches')}</span>
          </div>
        ) : (!riskErr && <div className="text-faint" style={{ fontSize: 11 }}>{t.tx('查询中…', 'Querying…')}</div>)}
      </div>
      <div className="card" style={{ padding: 16 }}>
        <h4 style={{ marginTop: 0 }}>{t.tx('风控命中明细', 'Risk-rule hits')}</h4>
        <div className="text-faint" style={{ fontSize: 11 }} data-todo="endpoint:/api/admin/risk/hits?user_id={id}">—</div>
      </div>
      <div className="card" style={{ padding: 16 }}>
        <h4 style={{ marginTop: 0 }}>{t.tx('黑名单/限制列表', 'Blacklist / restrictions')}</h4>
        <div className="text-faint" style={{ fontSize: 11 }} data-todo="endpoint:/api/risk/blacklist?value={id}">—</div>
      </div>
      <div className="card" style={{ padding: 16 }}>
        <h4 style={{ marginTop: 0 }}>{t.tx('关联设备/账号', 'Linked devices / accounts')}</h4>
        <div className="text-faint" style={{ fontSize: 11 }} data-todo="endpoint:/api/admin/risk/links?user_id={id}">—</div>
      </div>
    </div>
  );
}

// ─── Tab 13: 客服/工单 ──────────────────────────────────────────────────────
function UserSupportTab({ user, t, push }) {
  return (
    <div className="stack">
      <h3>{t.tx('客服/工单', 'Support / Tickets')}</h3>
      <div className="card" style={{ padding: 16 }}>
        <h4 style={{ marginTop: 0 }}>{t.tx('工单列表', 'Tickets')}</h4>
        <div className="text-faint" style={{ fontSize: 11 }} data-todo="endpoint:/api/admin/tickets?user_id={id}">—</div>
      </div>
      <div className="card" style={{ padding: 16 }}>
        <h4 style={{ marginTop: 0 }}>{t.tx('在线咨询历史', 'Chat history')}</h4>
        <div className="text-faint" style={{ fontSize: 11 }} data-todo="endpoint:/api/admin/support/chat?user_id={id}">—</div>
      </div>
      <div className="card" style={{ padding: 16 }}>
        <h4 style={{ marginTop: 0 }}>{t.tx('投诉与申诉', 'Complaints')}</h4>
        <div className="text-faint" style={{ fontSize: 11 }} data-todo="endpoint:/api/admin/complaints?user_id={id}">—</div>
      </div>
      <div className="card" style={{ padding: 16 }}>
        <h4 style={{ marginTop: 0 }}>{t.tx('满意度评分', 'CSAT')}</h4>
        <div className="text-faint" style={{ fontSize: 11 }} data-todo="endpoint:/api/admin/users/{id}/csat">—</div>
      </div>
    </div>
  );
}

// ─── Tab 14: 操作日志（rewired from events → audit） ────────────────────────
function UserAuditTab({ user, t, push }) {
  const [audit] = _useAdminPoll('/api/audit?limit=200', 12000, true);
  const all = (audit && (audit.entries || audit.audit || audit)) || [];
  const list = Array.isArray(all) ? all : [];
  const filtered = list.filter(a => {
    const blob = JSON.stringify(a);
    return blob.includes(user.id);
  });

  return (
    <div className="card">
      <div className="filter-bar">
        <span className="text-muted" style={{ fontSize: 11 }}>{t.tx('从 /api/audit 最近 200 条 filter user_id=', 'Filter last 200 of /api/audit by user_id=')}{user.id}</span>
        <div style={{ flex: 1 }} />
        <span className="text-faint" style={{ fontSize: 11 }}>{t.tx('命中', 'Match')} {filtered.length} / {list.length}</span>
      </div>
      <div className="tbl-scroll">
      <table className="tbl tbl-stack">
        <thead><tr>
          <th>{t.tx('时间', 'Time')}</th><th>actor</th><th>action</th><th>target</th><th>{t.tx('详情', 'Detail')}</th>
        </tr></thead>
        <tbody>
          {!audit && <tr><td colSpan={5} style={{ textAlign: 'center', padding: 24, color: 'var(--text-muted)' }}>{t.tx('加载中…', 'Loading…')}</td></tr>}
          {audit && filtered.length === 0 && <tr><td colSpan={5} style={{ textAlign: 'center', padding: 24, color: 'var(--text-muted)' }}>{t.tx('该用户暂无审计记录', 'No audit records for this user')}</td></tr>}
          {filtered.map((a, i) => (
            <tr key={a.id || i}>
              <td data-label={t.tx('时间', 'Time')} className="muted" style={{ fontSize: 11 }}>{_relTime(a.ts || a.created_at || a.time)}</td>
              <td data-label="actor" className="text-mono" style={{ fontSize: 11 }}>{a.actor || a.admin || a.who || '—'}</td>
              <td data-label="action"><span className="badge outline" style={{ fontSize: 10 }}>{a.action || a.kind || '?'}</span></td>
              <td data-label="target" className="text-mono" style={{ fontSize: 11 }}>{a.target || a.target_id || '—'}</td>
              <td data-label={t.tx('详情', 'Detail')} className="text-faint" style={{ fontSize: 10.5, fontFamily: 'var(--font-mono)' }}>
                {(() => {
                  const extras = Object.assign({}, a);
                  ['id','ts','created_at','time','actor','admin','who','action','kind','target','target_id'].forEach(k => delete extras[k]);
                  return JSON.stringify(extras);
                })()}
              </td>
            </tr>
          ))}
        </tbody>
      </table>
      </div>
    </div>
  );
}

// ─── Wallet adjust modal ────────────────────────────────────────────────────
function WalletAdjustModal({ user, t, onClose, push }) {
  const [amount, setAmount] = React.useState('');
  const [note, setNote] = React.useState('');
  const [busy, setBusy] = React.useState(false);
  const [err, setErr] = React.useState('');

  const submit = async () => {
    setErr('');
    const amt = parseFloat(amount);
    if (!isFinite(amt) || amt === 0) { setErr(t.tx('金额必须是非零数字（正充负扣）', 'Amount must be non-zero (positive credits, negative debits)')); return; }
    if (amt > 1_000_000 || amt < -1_000_000) { setErr(t.tx('单次调整额度 ≤ ¥1,000,000', 'Per-adjustment limit ≤ ¥1,000,000')); return; }
    setBusy(true);
    try {
      const r = await _adminFetch(`/api/wallet/${encodeURIComponent(user.id)}/adjust`, {
        method: 'POST',
        body: JSON.stringify({ amount: amt, note: note.trim() || 'admin adjust' }),
      });
      const newBal = r && r.wallet ? r.wallet.balance : null;
      push(t.tx(
        `已调整 ${user.id} · ${amt > 0 ? '+' : ''}${fmtCN(amt)}${newBal != null ? ` · 新余额 ${fmtCN(newBal)}` : ''}`,
        `Adjusted ${user.id} · ${amt > 0 ? '+' : ''}${fmtCN(amt)}${newBal != null ? ` · new balance ${fmtCN(newBal)}` : ''}`
      ));
      onClose();
    } catch (e) {
      setErr((e.body && e.body.error) || e.message || t.tx('调整失败', 'Adjustment failed'));
    } finally {
      setBusy(false);
    }
  };

  return (
    <Modal open={true} onClose={onClose} title={t.tx(`调整钱包余额 · ${user.id}`, `Adjust wallet balance · ${user.id}`)} width={460}
           footer={<>
             <button className="btn btn-sm" onClick={onClose} disabled={busy}>{t.tx('取消', 'Cancel')}</button>
             <button className="btn btn-pri btn-sm" onClick={submit} disabled={busy}>{busy ? t.tx('提交中…', 'Submitting…') : t.tx('确认调整', 'Confirm adjustment')}</button>
           </>}>
      <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
        <div className="text-faint" style={{ fontSize: 11 }}>
          {t.tx('当前余额', 'Current balance')} <span className="tabular text-mono" style={{ color: 'var(--text)' }}>{fmtCN(user.balance || 0)}</span>
        </div>
        <Field label={t.tx('金额（正充 / 负扣，¥）', 'Amount (positive=credit / negative=debit, ¥)')}>
          <input className="input" value={amount} onChange={(e) => setAmount(e.target.value)} placeholder={t.tx('例如 100 或 -50', 'e.g. 100 or -50')} inputMode="decimal" autoFocus />
        </Field>
        <Field label={t.tx('备注', 'Note')}>
          <input className="input" value={note} onChange={(e) => setNote(e.target.value)} placeholder={t.tx('调账原因，会写入 ledger.note', 'Adjustment reason, written to ledger.note')} />
        </Field>
        {err && <div style={{ color: 'var(--danger)', fontSize: 12 }}>{err}</div>}
        <div className="text-faint" style={{ fontSize: 11 }}>
          {t.tx('调用', 'Calls')} <span className="text-mono">POST /api/wallet/{user.id}/adjust</span> · {t.tx('写入 ledger（type=adjust）并触发审计。', 'Writes to ledger (type=adjust) and triggers audit.')}
        </div>
      </div>
    </Modal>
  );
}

// ─────────────────────────────────────────────────────────────────────────────
// 08 MEMBER TIERS — 等级阶梯 + leaderboard
// ─────────────────────────────────────────────────────────────────────────────
const TIER_LADDER = [
  { key: 'normal',  name: '普通会员', nameEn: 'Standard', growth: 0,      wd: 10000,   fee: '0.5%',
    perks:   ['基础提现 ¥10k/笔', '0.5% 手续费', '普通客服'],
    perksEn: ['Withdraw ¥10k/tx', '0.5% fee', 'Standard support'] },
  { key: 'bronze',  name: '青铜',    nameEn: 'Bronze',   growth: 1000,   wd: 30000,   fee: '0.4%',
    perks:   ['提现 ¥30k/笔', '0.4% 手续费', '签到奖励 ×1.2'],
    perksEn: ['Withdraw ¥30k/tx', '0.4% fee', 'Check-in bonus ×1.2'] },
  { key: 'silver',  name: '白银',    nameEn: 'Silver',   growth: 8000,   wd: 60000,   fee: '0.3%',
    perks:   ['提现 ¥60k/笔', '0.3% 手续费', '专属活动'],
    perksEn: ['Withdraw ¥60k/tx', '0.3% fee', 'Exclusive events'] },
  { key: 'gold',    name: '黄金',    nameEn: 'Gold',     growth: 40000,  wd: 120000,  fee: '0.2%',
    perks:   ['提现 ¥120k/笔', '0.2% 手续费', 'VIP 队列', '充值 ×1.5'],
    perksEn: ['Withdraw ¥120k/tx', '0.2% fee', 'VIP queue', 'Deposit ×1.5'] },
  { key: 'diamond', name: '钻石',    nameEn: 'Diamond',  growth: 200000, wd: 500000,  fee: '0.1%',
    perks:   ['提现 ¥500k/笔', '0.1% 手续费', '专属客服', '大额通道'],
    perksEn: ['Withdraw ¥500k/tx', '0.1% fee', 'Dedicated support', 'Large-amount channel'] },
  { key: 'supreme', name: '至尊',    nameEn: 'Supreme',  growth: 800000, wd: 2000000, fee: '0%',
    perks:   ['提现 ¥2M/笔', '免手续费', '1v1 客户经理', 'USDT 通道'],
    perksEn: ['Withdraw ¥2M/tx', 'No fees', '1-on-1 account manager', 'USDT channel'] },
];

function LevelsScreen({ t, push }) {
  const [by, setBy] = React.useState('recharge'); // recharge | stake
  const [data, err] = _useAdminPoll(`/api/membership/leaderboard?by=${by}&limit=100`, 15000, true);

  const list = (data && data.leaderboard) || [];

  return (
    <div className="stack">
      <div className="page-h">
        <div>
          <h1>{t.p_levels}</h1>
          <div className="sub">{t.tx('6 级 VIP 阶梯 · Top 100 排行榜', '6-tier VIP ladder · Top 100 leaderboard')} · {by === 'recharge' ? t.tx('按累计充值', 'by total deposit') : t.tx('按累计流水', 'by total stake')}</div>
        </div>
        <div className="actions">
          <select className="select" style={{ width: 140 }} value={by} onChange={(e) => setBy(e.target.value)}>
            <option value="recharge">{t.tx('累计充值', 'Total deposit')}</option>
            <option value="stake">{t.tx('累计流水', 'Total stake')}</option>
          </select>
        </div>
      </div>

      {/* Tier ladder strip (hardcoded benefits — backend lacks tier-config API) */}
      <div className="card" style={{ padding: 18 }}>
        <div style={{ position: 'relative' }}>
          <div style={{ position: 'absolute', left: 0, right: 0, top: 28, height: 2, background: 'linear-gradient(to right, oklch(0.85 0.04 50), oklch(0.85 0.08 85), oklch(0.78 0.12 220), oklch(0.6 0.18 305))' }} />
          <div style={{ display: 'grid', gridTemplateColumns: 'repeat(6, 1fr)', position: 'relative' }}>
            {TIER_LADDER.map((tier) => (
              <div key={tier.key} style={{ textAlign: 'center', padding: '4px 4px 0' }}>
                <div style={{
                  width: 56, height: 56, borderRadius: '50%', background: 'var(--bg-card)',
                  border: '2px solid var(--border)',
                  display: 'flex', alignItems: 'center', justifyContent: 'center',
                  margin: '0 auto 8px', position: 'relative', zIndex: 1,
                }}>
                  <Icons.vip />
                </div>
                <div style={{ fontSize: 13, fontWeight: 600 }}><TierBadge tier={tier.key} t={t} /></div>
                <div className="text-faint" style={{ fontSize: 10, marginTop: 2 }}>{tier.nameEn}</div>
                <div className="tabular text-muted" style={{ fontSize: 11, marginTop: 6 }}>
                  ≥ {tier.growth.toLocaleString()} {t.tx('充值', 'deposit')}
                </div>
                <div className="text-faint" style={{ fontSize: 10, marginTop: 4, padding: '0 6px', lineHeight: 1.4 }}>
                  {(t.lang === 'cn' ? tier.perks : tier.perksEn).map((p, i) => <div key={i}>{p}</div>)}
                </div>
              </div>
            ))}
          </div>
        </div>
      </div>

      {/* Leaderboard */}
      <div className="card">
        <div className="card-h">
          <h3>{t.tx('Top 100 排行榜', 'Top 100 leaderboard')}</h3>
          <span className="meta">/api/membership/leaderboard?by={by}</span>
        </div>
        {err && <div style={{ padding: 16, color: 'var(--danger)', fontSize: 12 }}>{t.tx('API 错误：', 'API error: ')}{err.message}</div>}
        <div className="tbl-scroll">
        <table className="tbl tbl-stack">
          <thead><tr>
            <th>#</th>
            <th>user_id</th>
            <th>{t.tx('名称', 'Name')}</th>
            <th>{t.tx('等级', 'Tier')}</th>
            <th className="right">{t.tx('累计充值', 'Total deposit')}</th>
            <th className="right">{t.tx('累计流水', 'Total stake')}</th>
          </tr></thead>
          <tbody>
            {!data && !err && <tr><td colSpan={6} style={{ textAlign: 'center', padding: 24, color: 'var(--text-muted)' }}>{t.tx('加载中…', 'Loading…')}</td></tr>}
            {data && list.length === 0 && <tr><td colSpan={6} style={{ textAlign: 'center', padding: 24, color: 'var(--text-muted)' }}>{t.tx('暂无数据', 'No data')}</td></tr>}
            {list.map((row, i) => {
              const lvl = row.level || _tierFromRecharge(row.accumulated_recharge);
              return (
                <tr key={row.user_id || i}>
                  <td data-label="#" className="text-faint tabular" style={{ width: 36 }}>{i + 1}</td>
                  <td data-label="user_id" className="text-mono" style={{ fontSize: 11.5 }}>{row.user_id}</td>
                  <td data-label={t.tx('名称', 'Name')}>{row.name || '—'}</td>
                  <td data-label={t.tx('等级', 'Tier')}><TierBadge tier={lvl} t={t} /></td>
                  <td data-label={t.tx('累计充值', 'Total deposit')} className="right tabular">{fmtCN(row.accumulated_recharge || 0)}</td>
                  <td data-label={t.tx('累计流水', 'Total stake')} className="right tabular">{fmtCN(row.accumulated_stake || 0)}</td>
                </tr>
              );
            })}
          </tbody>
        </table>
        </div>
        <div className="text-faint" style={{ padding: 10, fontSize: 11, borderTop: '1px solid var(--border)' }}>
          {t.tx('TODO：后端 leaderboard 仅支持 by=recharge | stake，points 维度未实现。等级阈值前端 hardcoded — 待后端 tier-config API。', 'TODO: backend leaderboard only supports by=recharge | stake; points dimension not implemented. Tier thresholds hardcoded in frontend — awaiting backend tier-config API.')}
        </div>
      </div>
    </div>
  );
}

// ─── Field (used by WalletAdjustModal) ───────────────────────────────────────
function Field({ label, children }) {
  return (
    <div className="field">
      <label>{label}</label>
      {children}
    </div>
  );
}

// ─────────────────────────────────────────────────────────────────────────────
// 09 AGENTS — mock 数据（后端暂无代理 API）
// AgentsScreen — 接 /api/agents（live PG-backed）+ POST/PUT/DELETE 管理操作。
// ─────────────────────────────────────────────────────────────────────────────
function AgentsScreen({ t, push }) {
  const [tick, setTick] = React.useState(0);
  const [statusFilter, setStatusFilter] = React.useState('');
  const [view, setView] = React.useState('overview'); // overview | commissions | tree
  const path = '/api/agents' + (statusFilter ? `?status=${statusFilter}` : '') + `&_=${tick}`;
  const [data, err] = window.NebulaAdmin.useAdminPoll(path, 15000, true);
  const agents = (data && data.agents) || [];

  const total = agents.length;
  const totalTeam = agents.reduce((s, a) => s + (a.team || 0), 0);
  const totalDeposit = agents.reduce((s, a) => s + (a.deposit || 0), 0);
  const totalProfit = agents.reduce((s, a) => s + (a.profit || 0), 0);
  const suspicious = agents.filter(a => a.status === 'warning' || a.status === 'risk').length;

  const doCreate = async () => {
    const id = window.prompt(t.tx('代理 ID（如 A-Zxxx）', 'Agent ID (e.g. A-Zxxx)'));
    if (!id) return;
    const name = window.prompt(t.tx('代理名称', 'Agent name'), t.tx('新代理', 'New agent'));
    if (!name) return;
    const source = window.prompt(t.tx('渠道（Telegram / WeChat / Facebook / Google / X / ...）', 'Channel (Telegram / WeChat / Facebook / Google / X / ...)'), 'Telegram') || '';
    const commStr = window.prompt(t.tx('分成比例 0..1', 'Commission rate 0..1'), '0.05') || '0.05';
    const comm = parseFloat(commStr) || 0.05;
    try {
      await window.NebulaAdmin.adminFetch('/api/admin/agents', {
        method: 'POST',
        body: JSON.stringify({ id, name, level: 3, source, comm_rate: comm, status: 'normal', quality: 'C' }),
      });
      push(t.tx(`已创建代理 ${id}`, `Created agent ${id}`));
      setTick(x => x + 1);
    } catch (e) { push(t.tx(`创建失败：${e.body?.error || e.status || ''}`, `Create failed: ${e.body?.error || e.status || ''}`)); }
  };

  const doBan = async (a) => {
    if (!window.confirm(t.tx(`确定下架代理 ${a.name} (${a.id}) 吗？`, `Remove agent ${a.name} (${a.id})?`))) return;
    try {
      await window.NebulaAdmin.adminFetch(`/api/admin/agents/${encodeURIComponent(a.id)}`, { method: 'DELETE' });
      push(t.tx(`已下架 ${a.id}`, `Removed ${a.id}`));
      setTick(x => x + 1);
    } catch (e) { push(t.tx(`下架失败：${e.body?.error || e.status || ''}`, `Remove failed: ${e.body?.error || e.status || ''}`)); }
  };

  return (
    <div className="stack">
      <div className="page-h">
        <div>
          <h1>{t.p_agents}</h1>
          <div className="sub">
            {total} {t.tx('个代理 · 团队总规模', 'agents · total team size')} {fmtNum(totalTeam)} {t.tx('人', 'people')}
            {err ? <span className="badge danger" style={{ fontSize: 10, marginLeft: 6 }}>{t.tx('API 离线', 'API offline')}</span> : null}
          </div>
        </div>
        <div className="actions">
          <button className="btn btn-pri btn-sm" onClick={doCreate}><Icons.plus /> {t.tx('新建代理', 'New agent')}</button>
        </div>
      </div>

      <div className="row-4">
        <KPI label={t.tx('代理总数', 'Total agents')} value={fmtNum(total)} delta={'L1: ' + agents.filter(a => a.level === 1).length} up t={t} />
        <KPI label={t.tx('总团队规模', 'Total team size')} value={fmtNum(totalTeam)} delta={t.tx('累计 ', 'Total ') + fmtCN(totalDeposit)} up t={t} />
        <KPI label={t.tx('近 30 天利润', 'Profit (30d)')} value={fmtCN(totalProfit)} delta={totalProfit > 0 ? '+' + totalProfit.toFixed(0) : totalProfit.toFixed(0)} up={totalProfit > 0} t={t} />
        <KPI label={t.tx('可疑渠道', 'Suspicious channels')} value={String(suspicious)} delta={suspicious > 0 ? t.tx('需关注', 'Needs review') : t.tx('无', 'None')} t={t} />
      </div>

      <div className="filter-bar" style={{ gap: 6 }}>
        <span className={'chip ' + (view === 'overview' ? 'active' : '')} onClick={() => setView('overview')}>{t.tx('概览', 'Overview')}</span>
        <span className={'chip ' + (view === 'commissions' ? 'active' : '')} onClick={() => setView('commissions')}>{t.tx('佣金报表', 'Commissions report')}</span>
        <span className={'chip ' + (view === 'tree' ? 'active' : '')} onClick={() => setView('tree')}>{t.tx('团队树', 'Team tree')}</span>
      </div>

      {view === 'commissions' && <AgentsCommissionsTab t={t} push={push} />}
      {view === 'tree' && <AgentsTreeTab t={t} agents={agents} push={push} />}

      {view === 'overview' && (
      <div className="card">
        <div className="filter-bar">
          <span className={'chip ' + (statusFilter === '' ? 'active' : '')} onClick={() => setStatusFilter('')}>{t.all} {total}</span>
          <span className={'chip ' + (statusFilter === 'normal' ? 'active' : '')} onClick={() => setStatusFilter('normal')}>{t.tx('活跃', 'Active')}</span>
          <span className={'chip ' + (statusFilter === 'warning' ? 'active' : '')} onClick={() => setStatusFilter('warning')}>{t.tx('预警', 'Warning')}</span>
          <span className={'chip ' + (statusFilter === 'risk' ? 'active' : '')} onClick={() => setStatusFilter('risk')}>{t.tx('可疑', 'Suspicious')}</span>
          <span className={'chip ' + (statusFilter === 'banned' ? 'active' : '')} onClick={() => setStatusFilter('banned')}>{t.tx('已封禁', 'Banned')}</span>
          <div style={{ flex: 1 }} />
        </div>
        <div className="tbl-scroll">
        <table className="tbl tbl-stack">
          <thead><tr>
            <th>{t.tx('代理', 'Agent')}</th><th>{t.tx('层级', 'Level')}</th><th>{t.tx('渠道', 'Channel')}</th><th className="right">{t.tx('团队', 'Team')}</th><th className="right">{t.tx('充值(30d)', 'Deposit(30d)')}</th>
            <th className="right">{t.tx('利润(30d)', 'Profit(30d)')}</th><th>{t.tx('分成', 'Comm')}</th><th>{t.tx('质量', 'Quality')}</th><th>{t.tx('状态', 'Status')}</th><th></th>
          </tr></thead>
          <tbody>
            {agents.length === 0 ? (
              <tr><td colSpan={10} className="text-faint" style={{ textAlign: 'center', padding: 20 }}>
                {data === undefined ? t.tx('加载中…', 'Loading…') : t.tx('暂无代理', 'No agents')}
              </td></tr>
            ) : agents.map((a) => (
              <tr key={a.id}>
                <td data-label={t.tx('代理', 'Agent')}><div style={{ display: 'flex', gap: 8, alignItems: 'center' }}><Avatar name={a.name} size={26} /><div><div style={{ fontWeight: 500 }}>{a.name}</div><div className="text-mono text-faint" style={{ fontSize: 10 }}>{a.id}</div></div></div></td>
                <td data-label={t.tx('层级', 'Level')}><span className="badge outline">L{a.level}</span></td>
                <td data-label={t.tx('渠道', 'Channel')}>{a.source || '—'}</td>
                <td data-label={t.tx('团队', 'Team')} className="right tabular">{fmtNum(a.team)}</td>
                <td data-label={t.tx('充值(30d)', 'Deposit(30d)')} className="right tabular">{fmtCN(a.deposit)}</td>
                <td data-label={t.tx('利润(30d)', 'Profit(30d)')} className="right tabular" style={{ color: a.profit > 0 ? 'var(--success)' : 'var(--danger)' }}>{fmtCN(a.profit)}</td>
                <td data-label={t.tx('分成', 'Comm')}>{a.comm}</td>
                <td data-label={t.tx('质量', 'Quality')}>
                  <span className={'badge ' + (a.quality === 'A+' ? 'success' : a.quality === 'A' ? 'success' : a.quality === 'B' ? 'info' : a.quality === 'C' ? 'warning' : 'danger')}>
                    {a.quality}
                  </span>
                </td>
                <td data-label={t.tx('状态', 'Status')}><StatusBadge status={a.status} t={t} /></td>
                <td data-label={t.tx('操作', 'Actions')} className="tbl-actions">
                  {a.status !== 'banned' && (
                    <button className="btn btn-xs btn-danger" onClick={() => doBan(a)}>{t.tx('下架', 'Remove')}</button>
                  )}
                </td>
              </tr>
            ))}
          </tbody>
        </table>
        </div>
        <div className="text-faint" style={{ padding: 10, fontSize: 11, borderTop: '1px solid var(--border)' }}>
          {t.tx('数据来自 /api/agents（15s 自动刷新）。佣金结算流程未做（TODO）。', 'Data from /api/agents (15s auto-refresh). Commission settlement flow not implemented (TODO).')}
        </div>
      </div>
      )}
    </div>
  );
}

// ─────────────────────────────────────────────────────────────────────────────
// AgentsCommissionsTab — 佣金报表
// /api/admin/agents/commissions-report?days=N  → { daily, top_earners }
// ─────────────────────────────────────────────────────────────────────────────
function AgentsCommissionsTab({ t, push }) {
  const [days, setDays] = React.useState(7);
  const [data, err] = window.NebulaAdmin.useAdminPoll(`/api/admin/agents/commissions-report?days=${days}`, 30000, true);
  const daily = (data && data.daily) || [];
  const top = (data && data.top_earners) || [];
  const totalWindow = daily.reduce((s, d) => s + (d.total || 0), 0);
  const dailyVals = daily.map(d => d.total || 0);
  const maxDay = Math.max(...dailyVals, 1);

  return (
    <div className="card" style={{ padding: 14 }}>
      <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 10 }}>
        <div>
          <div style={{ fontSize: 14, fontWeight: 600 }}>{t.tx('佣金报表', 'Commissions report')}</div>
          <div className="text-faint" style={{ fontSize: 11 }}>
            {t.tx('近 ', 'Last ')}{days}{t.tx(' 天合计 ', ' days total ')}{fmtCN(totalWindow)}
            {err ? <span className="badge danger" style={{ fontSize: 10, marginLeft: 6 }}>{t.tx('API 错误', 'API error')}</span> : null}
          </div>
        </div>
        <div style={{ display: 'flex', gap: 4 }}>
          {[7, 14, 30].map(d => (
            <span key={d} className={'chip ' + (days === d ? 'active' : '')} onClick={() => setDays(d)}>{d}{t.tx('天', 'd')}</span>
          ))}
        </div>
      </div>

      <div style={{ display: 'flex', alignItems: 'flex-end', gap: 4, height: 110, padding: '6px 0', borderBottom: '1px solid var(--border)' }}>
        {daily.length === 0 ? (
          <div style={{ flex: 1, textAlign: 'center', color: 'var(--text-muted)', fontSize: 12 }}>
            {data === null ? t.tx('加载中…', 'Loading…') : t.tx('暂无数据', 'No data')}
          </div>
        ) : daily.map((d, i) => {
          const h = Math.max(2, (d.total / maxDay) * 90);
          return (
            <div key={d.date || i} style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 4 }}>
              <div title={`${d.date}: ${fmtCN(d.total)}`}
                   style={{ width: '70%', height: h, background: 'var(--accent)', borderRadius: '3px 3px 0 0', opacity: 0.85 }} />
              <div className="text-faint" style={{ fontSize: 9 }}>{(d.date || '').slice(5)}</div>
            </div>
          );
        })}
      </div>

      <div style={{ marginTop: 14 }}>
        <div style={{ fontSize: 12, fontWeight: 600, marginBottom: 8 }}>{t.tx('本期 Top 收益', 'Top earners this window')}</div>
        <div className="tbl-scroll">
        <table className="tbl tbl-stack">
          <thead><tr>
            <th>{t.tx('用户', 'User')}</th>
            <th className="right">{t.tx('累计佣金', 'Total commission')}</th>
          </tr></thead>
          <tbody>
            {top.length === 0 ? (
              <tr><td colSpan={2} className="text-faint" style={{ textAlign: 'center', padding: 16 }}>
                {data === null ? t.tx('加载中…', 'Loading…') : t.tx('暂无收益记录', 'No earnings yet')}
              </td></tr>
            ) : top.map((e) => (
              <tr key={e.user_id}>
                <td data-label={t.tx('用户', 'User')}><div style={{ display: 'flex', gap: 8, alignItems: 'center' }}><Avatar name={e.user_id} size={22} /><span className="text-mono">{e.user_id}</span></div></td>
                <td data-label={t.tx('累计佣金', 'Total commission')} className="right tabular" style={{ color: 'var(--success)' }}>{fmtCN(e.total)}</td>
              </tr>
            ))}
          </tbody>
        </table>
        </div>
      </div>
    </div>
  );
}

// ─────────────────────────────────────────────────────────────────────────────
// AgentsTreeTab — 团队树状视图
// Pick an agent, fetch downline via /api/agent/referral-stats (admin can read
// the endpoint since RequireRole includes admin). For the 3-level tree we
// re-query against /api/agents/{id} for the agent header + display its
// "team" virtual hierarchy reused from the existing aggregates field. The
// underlying user-level lineage lives in user_referrals; we render a
// placeholder tree built from the agents.team scaling.
// ─────────────────────────────────────────────────────────────────────────────
function AgentsTreeTab({ t, agents, push }) {
  const [pickedId, setPickedId] = React.useState(agents[0]?.id || '');
  React.useEffect(() => {
    if (!pickedId && agents[0]) setPickedId(agents[0].id);
  }, [agents, pickedId]);
  const picked = agents.find(a => a.id === pickedId);

  // Synthesise a 3-level breakdown from picked.team (real lineage requires
  // user_referrals joins beyond this page's scope). 60% L1 / 30% L2 / 10% L3.
  const team = picked?.team || 0;
  const l1 = Math.round(team * 0.6);
  const l2 = Math.round(team * 0.3);
  const l3 = Math.max(0, team - l1 - l2);

  return (
    <div className="card" style={{ padding: 14 }}>
      <div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 12 }}>
        <div style={{ fontSize: 14, fontWeight: 600 }}>{t.tx('团队树状视图', 'Team tree view')}</div>
        <select value={pickedId} onChange={(e) => setPickedId(e.target.value)}
                style={{ marginLeft: 'auto', padding: '5px 10px', borderRadius: 6, background: 'var(--bg-card)', color: 'var(--text)', border: '1px solid var(--border)', fontSize: 12 }}>
          {agents.length === 0 && <option value="">{t.tx('暂无代理', 'No agents')}</option>}
          {agents.map(a => <option key={a.id} value={a.id}>{a.name} ({a.id})</option>)}
        </select>
      </div>

      {!picked ? (
        <div className="text-faint" style={{ textAlign: 'center', padding: 24, fontSize: 12 }}>
          {t.tx('请选择代理', 'Select an agent above')}
        </div>
      ) : (
        <div>
          <div style={{ display: 'flex', alignItems: 'center', gap: 10, padding: 12, background: 'var(--bg-soft)', borderRadius: 8, marginBottom: 12 }}>
            <Avatar name={picked.name} size={36} />
            <div style={{ flex: 1 }}>
              <div style={{ fontWeight: 600 }}>{picked.name}</div>
              <div className="text-mono text-faint" style={{ fontSize: 10 }}>{picked.id}</div>
            </div>
            <div style={{ textAlign: 'right' }}>
              <div className="tabular" style={{ fontWeight: 600 }}>{fmtNum(team)}</div>
              <div className="text-faint" style={{ fontSize: 10 }}>{t.tx('团队人数', 'Team size')}</div>
            </div>
          </div>

          <TreeLevel label={t.tx('一级直属', 'Level 1 (direct)')} count={l1} color="var(--success)" t={t} />
          <TreeLevel label={t.tx('二级团队', 'Level 2')} count={l2} color="var(--accent)" t={t} />
          <TreeLevel label={t.tx('三级团队', 'Level 3')} count={l3} color="var(--warning)" t={t} />

          <div className="text-faint" style={{ fontSize: 11, marginTop: 12, padding: 10, borderTop: '1px solid var(--border)' }}>
            {t.tx('注：3 级人数按团队总规模 60/30/10 比例估算；真实用户级链路存储于 user_referrals 表，可在用户详情页钻取。',
                  'Note: counts are 60/30/10 estimates of total team size. The real user-level lineage lives in the user_referrals table — drill into a user from the user list for accurate paths.')}
          </div>
        </div>
      )}
    </div>
  );
}

function TreeLevel({ label, count, color, t }) {
  const dots = Math.min(count, 40);
  return (
    <div style={{ marginBottom: 12 }}>
      <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 6 }}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
          <span style={{ width: 8, height: 8, borderRadius: '50%', background: color, display: 'inline-block' }} />
          <span style={{ fontSize: 12, fontWeight: 600 }}>{label}</span>
        </div>
        <span className="tabular text-faint" style={{ fontSize: 11 }}>{fmtNum(count)} {t.tx('人', 'people')}</span>
      </div>
      <div style={{ display: 'flex', flexWrap: 'wrap', gap: 3, padding: '4px 0' }}>
        {Array.from({ length: dots }).map((_, i) => (
          <span key={i} style={{ width: 10, height: 10, borderRadius: 2, background: color, opacity: 0.5 + 0.5 * (1 - i / dots) }} />
        ))}
        {count > dots && <span className="text-faint" style={{ fontSize: 10, paddingLeft: 4 }}>+{count - dots}</span>}
      </div>
    </div>
  );
}

Object.assign(window, { UserScreen, LevelsScreen, AgentsScreen });
