// UserManagementTab — admin-only list + edit role/status/notes/quota for users. const UserManagementTab = ({ lang, currentUser }) => { const t = lang === 'zh'; const [users, setUsers] = React.useState([]); const [loading, setLoading] = React.useState(true); const [error, setError] = React.useState(null); const [editTarget, setEditTarget] = React.useState(null); // user obj for edit modal const [topupTarget, setTopupTarget] = React.useState(null); const [deleteTarget, setDeleteTarget] = React.useState(null); const load = React.useCallback(async () => { setLoading(true); setError(null); try { const res = await apiFetch('/admin/users'); if (!res.ok) { setError(`HTTP ${res.status}`); return; } setUsers(await res.json()); } catch (e) { setError(String(e)); } finally { setLoading(false); } }, []); React.useEffect(() => { load(); }, [load]); if (loading) return

{t ? '載入中…' : 'Loading…'}

; if (error) return

{error}

; return (
{users.map(u => ( ))}
{t ? '頭像' : 'Avatar'} {t ? '名稱' : 'Name'} Email {t ? '角色' : 'Role'} {t ? '狀態' : 'Status'} Provider {t ? '註冊日' : 'Created'} {t ? '上次登入' : 'Last login'} {t ? '累計查詢' : 'Total queries'} {t ? '剩餘額度' : 'Quota'} {t ? '備註' : 'Notes'} {t ? '操作' : 'Actions'}
{u.avatar_url ? :
{(u.name || u.email).slice(0,1).toUpperCase()}
}
{u.name || '—'} {u.email} {u.role} {u.status} {u.provider} {formatDate(u.created_at, lang)} {u.last_login_at ? formatDate(u.last_login_at, lang) : '—'} {u.total_queries.toLocaleString()} {u.quota_remaining} {u.notes || '—'} setEditTarget(u)}>{t ? '編輯' : 'Edit'} setTopupTarget(u)}>{t ? '加值' : 'Top up'} setDeleteTarget(u)} title={currentUser && currentUser.id === u.id ? (t ? '不能刪除自己' : 'Cannot delete your own account') : ''}> {t ? '刪除' : 'Delete'}
{editTarget && ( setEditTarget(null)} onSaved={() => { setEditTarget(null); load(); }} /> )} {topupTarget && ( setTopupTarget(null)} onSaved={() => { setTopupTarget(null); load(); }} /> )} {deleteTarget && ( setDeleteTarget(null)} onDeleted={() => { setDeleteTarget(null); load(); }} /> )}
); }; const th = { padding: '10px 12px', textAlign: 'left', whiteSpace: 'nowrap' }; const td = { padding: '10px 12px', verticalAlign: 'middle' }; function formatDate(iso, lang) { if (!iso) return '—'; const d = new Date(iso); if (Number.isNaN(d.getTime())) return iso; const y = d.getFullYear(); const m = String(d.getMonth() + 1).padStart(2, '0'); const dd = String(d.getDate()).padStart(2, '0'); return `${y}-${m}-${dd}`; } const EditUserModal = ({ lang, user, onClose, onSaved }) => { const t = lang === 'zh'; const [role, setRole] = React.useState(user.role); const [status, setStatus] = React.useState(user.status); const [notes, setNotes] = React.useState(user.notes || ''); const [saving, setSaving] = React.useState(false); const [error, setError] = React.useState(null); const submit = async () => { const body = {}; if (role !== user.role) body.role = role; if (status !== user.status) body.status = status; if ((notes || '') !== (user.notes || '')) body.notes = notes; if (Object.keys(body).length === 0) { onClose(); return; } setSaving(true); setError(null); try { const res = await apiFetch(`/admin/users/${user.id}`, { method: 'PATCH', body: JSON.stringify(body), }); if (!res.ok) { const b = await res.json().catch(() => ({})); setError(b?.detail?.detail || `HTTP ${res.status}`); return; } onSaved(); } finally { setSaving(false); } }; return (
{user.email}