// 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 (
{/* Filter chips */}
{filters.map(f => ( ))}
{error && (
{error}
)} {!rows && !error && (

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

)} {rows && rows.length === 0 && (

{filter === 'pending' ? (t ? '目前沒有 pending 的 quota 申請' : 'No pending quota requests') : (t ? `沒有 ${filter} 的申請` : `No ${filter} requests`)}

)} {rows && rows.length > 0 && (
{rows.map(r => { const isPending = r.status === 'pending'; const reasonShort = (r.reason || '').length > 100 ? r.reason.slice(0, 100) + '…' : r.reason; return ( ); })}
{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, }} /> handleApprove(r.id)} disabled={busy[r.id]}> {t ? `核准 +${amounts[r.id] ?? 30}` : `Approve +${amounts[r.id] ?? 30}`} setRejecting({ id: r.id, note: '' })} disabled={busy[r.id]}> {t ? '拒絕' : 'Reject'}
) : r.status === 'approved' ? ( +{r.granted_amount} ) : ( {t ? '已拒絕' : 'Rejected'} )} {actionError[r.id] && (
{actionError[r.id]}
)}
)} {/* Reject confirmation modal */} {rejecting && (
setRejecting(null)} style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.5)', zIndex: 100, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 16 }}>
e.stopPropagation()} style={{ background: TOKEN.surface, border: `1px solid ${TOKEN.surfaceBorder}`, borderRadius: 12, padding: 24, maxWidth: 440, width: '100%' }}>

{t ? '拒絕 Quota 申請' : 'Reject quota request'}

{t ? '拒絕原因(會寄給使用者—未來功能)' : 'Rejection reason (will be sent to user — future)'}