// Admin Quota Requests tab — list + approve/reject pending quota requests. const QuotaRequestsTab = ({ lang }) => { const t = lang === 'zh'; const [filter, setFilter] = React.useState('pending'); const [rows, setRows] = React.useState(null); const [error, setError] = React.useState(null); const [actionError, setActionError] = React.useState({}); // {id: msg} const [toast, setToast] = React.useState(null); const [amounts, setAmounts] = React.useState({}); // {id: number} const [rejecting, setRejecting] = React.useState(null); // {id, note} const [busy, setBusy] = React.useState({}); // {id: bool} const showToast = (msg) => { setToast(msg); setTimeout(() => setToast(null), 3000); }; const fetchRows = React.useCallback(async () => { setError(null); try { const res = await apiFetch(`/admin/quota-requests?status=${filter}`); if (!res.ok) throw new Error(`HTTP ${res.status}`); const data = await res.json(); setRows(data); } catch (err) { setError(err.message || String(err)); } }, [filter]); React.useEffect(() => { fetchRows(); }, [fetchRows]); const formatRel = (iso) => { try { const d = new Date(iso); const diffMs = Date.now() - d.getTime(); const h = Math.floor(diffMs / 3_600_000); if (h < 1) return t ? '不到 1 小時前' : '<1h ago'; if (h < 24) return t ? `${h} 小時前` : `${h}h ago`; const days = Math.floor(h / 24); return t ? `${days} 天前` : `${days}d ago`; } catch { return iso; } }; const handleApprove = async (id) => { if (busy[id]) return; const amount = parseInt(amounts[id] ?? '30', 10); if (!Number.isFinite(amount) || amount < 1) { setActionError(prev => ({ ...prev, [id]: t ? '金額必須 ≥ 1' : 'Amount must be ≥ 1' })); return; } setBusy(prev => ({ ...prev, [id]: true })); setActionError(prev => ({ ...prev, [id]: null })); try { const res = await apiFetch(`/admin/quota-requests/${id}/approve`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ amount }), }); if (res.status === 409) { showToast(t ? '此申請已被處理' : 'This request has been processed'); await fetchRows(); return; } if (!res.ok) { const body = await res.json().catch(() => null); throw new Error(body?.detail?.detail || `HTTP ${res.status}`); } showToast(t ? `已核准 +${amount}` : `Approved +${amount}`); await fetchRows(); } catch (err) { setActionError(prev => ({ ...prev, [id]: err.message || String(err) })); } finally { setBusy(prev => ({ ...prev, [id]: false })); } }; const handleReject = async () => { if (!rejecting || !rejecting.id) return; const id = rejecting.id; const note = (rejecting.note || '').trim(); if (note.length < 1) { setActionError(prev => ({ ...prev, [id]: t ? '請填寫拒絕理由' : 'Reason required' })); return; } setBusy(prev => ({ ...prev, [id]: true })); try { const res = await apiFetch(`/admin/quota-requests/${id}/reject`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ note }), }); if (res.status === 409) { showToast(t ? '此申請已被處理' : 'This request has been processed'); } else if (!res.ok) { const body = await res.json().catch(() => null); throw new Error(body?.detail?.detail || `HTTP ${res.status}`); } else { showToast(t ? '已拒絕' : 'Rejected'); } setRejecting(null); await fetchRows(); } catch (err) { setActionError(prev => ({ ...prev, [id]: err.message || String(err) })); } finally { setBusy(prev => ({ ...prev, [id]: false })); } }; const filters = [ { id: 'pending', label: t ? '待處理' : 'Pending' }, { id: 'approved', label: t ? '已核准' : 'Approved' }, { id: 'rejected', label: t ? '已拒絕' : 'Rejected' }, ]; return (
{t ? '載入中…' : 'Loading…'}
)} {rows && rows.length === 0 && ({filter === 'pending' ? (t ? '目前沒有 pending 的 quota 申請' : 'No pending quota requests') : (t ? `沒有 ${filter} 的申請` : `No ${filter} requests`)}
)} {rows && rows.length > 0 && (| {t ? '使用者' : 'User'} | {t ? '理由' : 'Reason'} | {t ? '送出時間' : 'Submitted'} | {t ? '剩餘額度' : 'Remaining'} | {t ? '操作' : 'Actions'} |
|---|---|---|---|---|
| {r.user_email} | {reasonShort} | {formatRel(r.requested_at)} | {r.user_quota_remaining} |
{isPending ? (
setAmounts(prev => ({ ...prev, [r.id]: e.target.value }))}
style={{
width: 70,
padding: '4px 8px',
borderRadius: 6,
border: `1px solid ${TOKEN.surfaceBorder}`,
background: TOKEN.surfaceRaised,
color: TOKEN.text,
fontSize: 13,
}} />
) : r.status === 'approved' ? (
+{r.granted_amount}
) : (
{t ? '已拒絕' : 'Rejected'}
)}
{actionError[r.id] && (
{actionError[r.id]}
)}
|
{t ? '拒絕原因(會寄給使用者—未來功能)' : 'Rejection reason (will be sent to user — future)'}