// ════════════════════════════════════════════════════════════════
// 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,
});