// ════════════════════════════════════════════════════════════════ // Admin atoms — переиспользуемые мелочи для всех админ-экранов // RoleBadge, StatusDot, TokenDisplay, ProgressBar, AdminQuickBar, // PageHeader, EmptyState, ConfirmRow // ════════════════════════════════════════════════════════════════ // ── 1. RoleBadge ──────────────────────────────────────────────── function RoleBadge({ role, size = 'md' }) { const cfg = window.SDH_ADMIN.ROLES.find(r => r.id === role); if (!cfg) return null; const sm = size === 'sm'; const styles = { display: 'inline-flex', alignItems: 'center', gap: sm ? 4 : 6, padding: sm ? '2px 7px' : '3px 9px', borderRadius: 'var(--radius-full)', fontSize: sm ? 10.5 : 11.5, fontWeight: role === 'super_admin' ? 600 : 500, background: cfg.hue === 'bronze' ? 'var(--brand-bronze-subtle)' : `var(--hue-${cfg.hue}-subtle)`, color: cfg.hue === 'bronze' ? 'var(--brand-bronze-deep)' : `var(--hue-${cfg.hue})`, border: `1px solid ${cfg.hue === 'bronze' ? 'var(--brand-bronze)' : `var(--hue-${cfg.hue})`}`, whiteSpace: 'nowrap', lineHeight: 1.3, }; return ( {cfg.marker} {cfg.label} ); } // ── 2. StatusDot ──────────────────────────────────────────────── function StatusDot({ status, label }) { const map = { active: { hue: 'emerald', text: label || 'активен' }, invited: { hue: 'amber', text: label || 'приглашён' }, pending: { hue: 'amber', text: label || 'ожидает' }, expired: { hue: 'rose', text: label || 'истёк' }, revoked: { hue: 'info', text: label || 'отозван' }, accepted: { hue: 'emerald', text: label || 'принято' }, running: { hue: 'emerald', text: label || 'работает' }, idle: { hue: 'info', text: label || 'простой' }, success: { hue: 'emerald', text: label || 'успех' }, failed: { hue: 'rose', text: label || 'ошибка' }, pulse: { hue: 'emerald', text: label || 'live', pulse: true }, }; const c = map[status] || map.idle; return ( {c.text} ); } // ── 3. TokenDisplay ───────────────────────────────────────────── function TokenDisplay({ value, label = 'Bearer token' }) { const [copied, setCopied] = React.useState(false); const [revealed, setRevealed] = React.useState(false); function copy() { if (navigator.clipboard) navigator.clipboard.writeText(value).catch(() => {}); setCopied(true); setTimeout(() => setCopied(false), 1600); } const masked = value.slice(0, 4) + '·'.repeat(Math.max(0, value.length - 8)) + value.slice(-4); return (
{label}
{revealed ? value : masked}
Сохраните токен в менеджер паролей. Увидеть снова не получится. При утере нужно регенерировать новый и обновить во всех клиентах.
); } // ── 4. UsageBar — прогресс с цветом по % ──────────────────────── function UsageBar({ used, total, height = 10, showLabel = true }) { const pct = total > 0 ? Math.min(1, used / total) : 0; const hue = pct < 0.75 ? 'emerald' : pct < 0.9 ? 'amber' : 'rose'; return (
{showLabel && (
{used.toLocaleString('ru-RU')} / {total.toLocaleString('ru-RU')} {Math.round(pct * 100)}%
)}
); } // ── 5. PageHeader — единый паттерн админ-экранов ──────────────── function PageHeader({ section, title, accent, subtitle, right }) { const renderTitle = () => { if (!accent) return title; const parts = title.split(accent); if (parts.length === 1) return title; return ( {parts[0]} {accent} {parts[1]} ); }; return (
{section}

{renderTitle()}

{subtitle && (

{subtitle}

)}
{right &&
{right}
}
); } // ── 6. EmptyState ─────────────────────────────────────────────── function EmptyState({ title, hint, action }) { return (

{title}

{hint &&

{hint}

} {action &&
{action}
}
); } // ── 7. AccessDenied — заглушка вместо страницы под чужой ролью ── function AccessDenied({ requiredRole, currentRole, onBack }) { return (
доступ ограничен

Этот раздел закрыт

Ваша роль — . Для доступа нужна роль или выше. Если вам нужен расширенный доступ, обратитесь к super-admin'у через профиль.

{onBack && ( )}
); } // ── 8. AdminQuickBar — pill-кнопки админу в Header ────────────── function AdminQuickBar({ role, route, onRoute, level = 'full' }) { if (role !== 'super_admin' && role !== 'admin') return null; const D = window.SDH_DATA; const A = window.SDH_ADMIN; const B = window.SDH_BILLING; const pendingInvites = A.invites.filter(i => i.status === 'pending').length; const expiringTokens = A.users.filter(u => u.token_warning).length; const cronOn = A.pipeline.cron_enabled && !A.pipeline.running ? 'idle' : A.pipeline.running ? 'running' : 'idle'; const monthCostUsd = B.adminSummary().monthCostUsd; const newReqs = role === 'super_admin' ? B.adminSummary().newReqsCount : 0; const totalBadge = pendingInvites + expiringTokens + newReqs; // Icon-only mode (narrow viewport): single Админка pill with combined badge if (level === 'icon') { const active = route.startsWith('admin'); return (
); } const compact = level === 'compact'; const items = compact ? [ { id: 'admin', label: 'Админка', active: route.startsWith('admin'), badge: totalBadge || null }, role === 'super_admin' && newReqs > 0 ? { id: 'admin.billing', label: 'Биллинг', active: route === 'admin.billing', badge: newReqs } : null, ].filter(Boolean) : [ { id: 'admin', label: 'Админка', active: route.startsWith('admin'), badge: (pendingInvites + expiringTokens) || null }, { id: 'admin.users', label: 'Пользователи', active: route === 'admin.users', count: A.users.filter(u => u.status === 'active').length }, { id: 'admin.pipeline',label: 'Pipeline', active: route === 'admin.pipeline', status: cronOn }, role === 'super_admin' && { id: 'admin.cost', label: `$${monthCostUsd.toFixed(2)}`, active: route === 'admin.cost' }, role === 'super_admin' && newReqs > 0 ? { id: 'admin.billing', label: 'Биллинг', active: route === 'admin.billing', badge: newReqs } : null, ].filter(Boolean); return (
{items.map(it => ( ))}
); } // ── 9. UserAvatar — кружок с инициалами ───────────────────────── function UserAvatar({ name, role, size = 32 }) { const initials = (name || '?') .split(/\s+/).slice(0, 2) .map(w => w.charAt(0).toUpperCase()).join(''); const cfg = window.SDH_ADMIN.ROLES.find(r => r.id === role) || { hue: 'info' }; const hueVar = cfg.hue === 'bronze' ? 'var(--brand-bronze)' : `var(--hue-${cfg.hue})`; const subVar = cfg.hue === 'bronze' ? 'var(--brand-bronze-subtle)' : `var(--hue-${cfg.hue}-subtle)`; return (
{initials}
); } // ── 10. KbdHint — подсказки клавиш ────────────────────────────── function KbdHint({ keys, label }) { return ( {keys.map((k, i) => ( {i > 0 && +} {k} ))} {label} ); } // ── Exports ───────────────────────────────────────────────────── Object.assign(window, { RoleBadge, StatusDot, TokenDisplay, UsageBar, PageHeader, EmptyState, AccessDenied, AdminQuickBar, UserAvatar, KbdHint, });