// 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 (
| {t ? '頭像' : 'Avatar'} |
{t ? '名稱' : 'Name'} |
Email |
{t ? '角色' : 'Role'} |
{t ? '狀態' : 'Status'} |
Provider |
{t ? '註冊日' : 'Created'} |
{t ? '上次登入' : 'Last login'} |
{t ? '累計查詢' : 'Total queries'} |
{t ? '剩餘額度' : 'Quota'} |
{t ? '備註' : 'Notes'} |
{t ? '操作' : 'Actions'} |
{users.map(u => (
{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}
{error && {error}
}
{t ? '取消' : 'Cancel'}
{saving ? (t ? '儲存中…' : 'Saving…') : (t ? '儲存' : 'Save')}
);
};
const TopUpModal = ({ lang, user, onClose, onSaved }) => {
const t = lang === 'zh';
const [delta, setDelta] = React.useState('100');
const [saving, setSaving] = React.useState(false);
const [error, setError] = React.useState(null);
const submit = async () => {
const n = Number.parseInt(delta, 10);
if (Number.isNaN(n)) { setError(t ? '請輸入整數' : 'Integer required'); return; }
setSaving(true);
setError(null);
try {
const res = await apiFetch(`/admin/users/${user.id}/quota`, {
method: 'PATCH',
body: JSON.stringify({ delta: n }),
});
if (!res.ok) {
const b = await res.json().catch(() => ({}));
setError(b?.detail?.detail || `HTTP ${res.status}`);
return;
}
onSaved();
} finally {
setSaving(false);
}
};
return (
{user.email}
{user.quota_remaining}
setDelta(e.target.value)} style={selectStyle} />
{error && {error}
}
{t ? '取消' : 'Cancel'}
{saving ? (t ? '處理中…' : 'Saving…') : (t ? '套用' : 'Apply')}
);
};
const DeleteUserModal = ({ lang, user, onClose, onDeleted }) => {
const t = lang === 'zh';
const [submitting, setSubmitting] = React.useState(false);
const [error, setError] = React.useState(null);
const submit = async () => {
setSubmitting(true);
setError(null);
try {
const res = await apiFetch(`/admin/users/${user.id}`, { method: 'DELETE' });
if (res.status !== 204 && !res.ok) {
const b = await res.json().catch(() => ({}));
setError(b?.detail?.detail || `HTTP ${res.status}`);
return;
}
onDeleted();
} finally {
setSubmitting(false);
}
};
return (
{t ? '確認刪除以下使用者?此動作無法復原。' : 'Confirm deleting this user? This cannot be undone.'}
{user.email}
{error && {error}
}
{t ? '取消' : 'Cancel'}
{submitting ? (t ? '刪除中…' : 'Deleting…') : (t ? '確認刪除' : 'Delete')}
);
};
const ModalShell = ({ title, onClose, children }) => (
);
const Field = ({ label, children }) => (
{children}
);
const selectStyle = {
width: '100%',
background: TOKEN.surfaceRaised,
border: `1px solid ${TOKEN.surfaceBorder}`,
borderRadius: 8,
padding: '8px 12px',
color: TOKEN.text,
fontSize: 14,
outline: 'none',
fontFamily: 'inherit',
};
const errorBox = {
background: 'rgba(239,68,68,0.12)',
border: '1px solid rgba(239,68,68,0.3)',
borderRadius: 8,
padding: '9px 13px',
color: '#f87171',
fontSize: 13,
marginTop: 8,
};
const modalActions = { display: 'flex', justifyContent: 'flex-end', gap: 8, marginTop: 16 };
Object.assign(window, { UserManagementTab });