import type { QuotaLineView, QuotaSubject, QuotaSubjectView } from '@dorfteich/shared'; import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { ApiError, apiDelete, apiGet, apiPut } from '../lib/api'; /** * Site-Admin quota override management (issue #58): look up a user or pond, see * every quota's effective value on the ladder, and set or clear a per-subject * override — which the QuotaService picks up at the next consumption check. The * usage column flags subjects currently over their effective limit. */ export function QuotaManager(): React.JSX.Element { const { t } = useTranslation('quotas'); const [type, setType] = useState('user'); const [q, setQ] = useState(''); const [subject, setSubject] = useState(null); const [drafts, setDrafts] = useState>({}); const [error, setError] = useState(null); const apply = (view: QuotaSubjectView): void => { setSubject(view); setDrafts( Object.fromEntries( view.lines.map((l) => [l.key, l.override === null ? '' : String(l.override)]), ), ); }; const find = async (): Promise => { setError(null); setSubject(null); try { const hit = await apiGet<{ id: string }>( `/admin/quotas/lookup?type=${type}&q=${encodeURIComponent(q.trim())}`, ); apply(await apiGet(`/admin/quotas/${type}/${hit.id}`)); } catch (err) { setError(err instanceof ApiError && err.status === 404 ? t('overrides.notFound') : 'error'); } }; const set = async (key: string): Promise => { if (!subject) return; const value = Number(drafts[key]); if (!Number.isFinite(value) || value < 0) return; apply( await apiPut(`/admin/quotas/${subject.type}/${subject.id}/${key}`, { value, }), ); }; const clear = async (key: string): Promise => { if (!subject) return; apply(await apiDelete(`/admin/quotas/${subject.type}/${subject.id}/${key}`)); }; return (

{t('overrides.title')}

setQ(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && void find()} />
{error && (

{error === 'error' ? t('overrides.notFound') : error}

)} {subject && ( <>

{subject.label}

{subject.lines.map((line) => ( setDrafts((d) => ({ ...d, [line.key]: v }))} onSet={() => void set(line.key)} onClear={() => void clear(line.key)} /> ))}
{t('overrides.key')} {t('overrides.default')} {t('overrides.override')} {t('overrides.effective')} {t('overrides.usage')}
)}
); } function QuotaRow({ line, draft, onDraft, onSet, onClear, }: { line: QuotaLineView; draft: string; onDraft: (v: string) => void; onSet: () => void; onClear: () => void; }): React.JSX.Element { const { t } = useTranslation('quotas'); const overQuota = line.usage !== null && line.usage > line.effective; return ( {t(`keys.${line.key}`)} {line.instanceDefault} onDraft(e.target.value)} /> {line.override !== null && ( )} {line.effective} {line.usage ?? '—'} {overQuota && · {t('overrides.overQuota')}} ); }