// Reusable RodaiClientes components const { useState, useEffect, useRef, useMemo, useCallback } = React; function Btn({ children, kind = 'ghost', size = 'md', icon, iconRight, onClick, disabled, title, type = 'button' }) { const cls = ['btn', `btn-${kind}`, size === 'sm' ? 'btn-sm' : ''].join(' '); return ( ); } function IconBtn({ name, onClick, title, size = 'md', kind = 'ghost' }) { const cls = ['btn', `btn-${kind}`, 'btn-icon', size === 'sm' ? 'btn-sm' : ''].join(' '); return ( ); } function Chip({ children, kind = 'default', icon }) { const cls = ['chip', kind !== 'default' ? `chip-${kind}` : ''].join(' '); return ( {icon ? : null} {children} ); } function StatusDot({ kind = 'mut', pulse = false }) { return ; } // Mini bars showing 4 servers' sync status for a client function MiniBars({ status }) { // status is array of 0=ok 1=syncing 2=diverged 3=missing const cls = (s) => { if (s === 0) return 's-ok'; if (s === 1) return 's-syncing'; if (s === 2) return 's-diverged'; if (s === 3) return 's-missing'; return 's-unknown'; }; return ( {status.map((s, i) => )} ); } function Input({ value, onChange, placeholder, icon, kbd, autoFocus, onKeyDown, type='text', style={}, maxLength }) { return ( ); } function Segmented({ value, onChange, options }) { return (
{options.map(o => ( ))}
); } function Sidebar({ view, setView, opsBadge }) { const items = [ { id: 'dashboard', label: 'Dashboard', icon: 'dashboard', kbd: 'g d' }, { id: 'clientes', label: 'Clientes', icon: 'users', kbd: 'g c' }, { id: 'times', label: 'Times', icon: 'tree', kbd: 'g t' }, { id: 'servidores',label: 'Servidores',icon: 'server', kbd: 'g s' }, { id: 'sync', label: 'Sincronização', icon: 'refresh', kbd: 'g y' }, { id: 'historico', label: 'Histórico', icon: 'history', kbd: 'g h' }, ]; return ( ); } function Topbar({ title, crumbs, actions, info }) { return (
{crumbs ? (
{crumbs.map((c, i) => ( {i > 0 ? / : null} {i === crumbs.length - 1 ? {c} : {c}} ))}
) : null}

{title}

{info}
{actions}
); } // Live operation banner for top of any screen when an op is running function LiveOpBanner({ op, onOpen }) { if (!op) return null; const total = op.servers.length; const done = op.servers.filter(s => s.state === 'done').length; const failed = op.servers.filter(s => s.state === 'fail').length; const running = op.servers.filter(s => s.state === 'running').length; return (
{op.kind}{' '} ·{' '} {op.target}{' '} em andamento — {done}/{total} servidores prontos · {running} rodando · {failed > 0 ? {failed} falhou : '0 falhou'}
Ver progresso
); } // Floating toast manager function Toast({ toast, onClose }) { if (!toast) return null; return (
{toast.title} {toast.body ? {toast.body} : null}
); } function ConfirmModal({ open, title, body, danger, confirmLabel = 'Confirmar', onConfirm, onCancel, children }) { useEffect(() => { if (!open) return; const onKey = (e) => { if (e.key === 'Escape') onCancel?.(); }; window.addEventListener('keydown', onKey); return () => window.removeEventListener('keydown', onKey); }, [open, onCancel]); if (!open) return null; return (
e.stopPropagation()}>
{danger ? : null} {title}
{body ?

{body}

: null}
{children ?
{children}
: null}
Cancelar {confirmLabel}
); } function CmdK({ open, onClose, gotoView }) { const [q, setQ] = useState(''); const inputRef = useRef(null); useEffect(() => { if (open) { setQ(''); setTimeout(() => inputRef.current?.focus(), 0); } const onKey = (e) => { if (e.key === 'Escape') onClose(); }; if (open) window.addEventListener('keydown', onKey); return () => window.removeEventListener('keydown', onKey); }, [open, onClose]); const items = useMemo(() => { const navItems = [ { id: 'go-dashboard', label: 'Ir para Dashboard', icon: 'dashboard', section: 'Navegação', run: () => gotoView('dashboard') }, { id: 'go-clientes', label: 'Ir para Clientes', icon: 'users', section: 'Navegação', run: () => gotoView('clientes') }, { id: 'go-times', label: 'Ir para Times', icon: 'tree', section: 'Navegação', run: () => gotoView('times') }, { id: 'go-servidores',label: 'Ir para Servidores', icon: 'server', section: 'Navegação', run: () => gotoView('servidores') }, { id: 'go-sync', label: 'Ir para Sincronização', icon: 'refresh', section: 'Navegação', run: () => gotoView('sync') }, ]; const actions = [ { id: 'a-new', label: 'Criar cliente novo', icon: 'plus', section: 'Ações', shortcut: 'N', run: () => gotoView('criar') }, { id: 'a-sync', label: 'Rodar verificação completa', icon: 'refresh', section: 'Ações', run: () => gotoView('sync') }, { id: 'a-op', label: 'Ver operação em andamento', icon: 'bolt', section: 'Ações', run: () => gotoView('operacao') }, ]; const clientItems = window.RC.clients.slice(0, 12).map(c => ({ id: 'c-' + c.id, label: c.name, sub: (window.RC.teamById(c.team)?.name || String(c.team)).toUpperCase(), icon: 'user', section: 'Clientes', run: () => gotoView('clientes', { selectId: c.id }), })); const all = [...navItems, ...actions, ...clientItems]; if (!q) return all; const ql = q.toLowerCase(); return all.filter(it => it.label.toLowerCase().includes(ql) || (it.sub || '').toLowerCase().includes(ql)); }, [q, gotoView]); const [active, setActive] = useState(0); useEffect(() => { setActive(0); }, [q, open]); if (!open) return null; const onKey = (e) => { if (e.key === 'ArrowDown') { e.preventDefault(); setActive(a => Math.min(items.length - 1, a + 1)); } if (e.key === 'ArrowUp') { e.preventDefault(); setActive(a => Math.max(0, a - 1)); } if (e.key === 'Enter') { e.preventDefault(); items[active]?.run(); onClose(); } }; // group by section const groups = {}; items.forEach((it, i) => { (groups[it.section] = groups[it.section] || []).push({ ...it, _i: i }); }); return (
e.stopPropagation()} >
setQ(e.target.value)} onKeyDown={onKey} placeholder="Buscar comandos, clientes, times…" style={{ all: 'unset', flex: 1, fontSize: 14, color: 'var(--text)' }} /> esc
{Object.keys(groups).length === 0 ? (
Sem resultados.
) : Object.entries(groups).map(([sec, list]) => (
{sec}
{list.map(it => (
setActive(it._i)} onClick={() => { it.run(); onClose(); }} className="row" style={{ padding: '6px 12px', gap: 10, cursor: 'pointer', height: 30, background: active === it._i ? 'var(--bg-active)' : 'transparent', }} > {it.label} {it.sub ? {it.sub} : null} {it.shortcut ? {it.shortcut} : null}
))}
))}
navegar selecionar esc fechar
); } Object.assign(window, { Btn, IconBtn, Chip, StatusDot, MiniBars, Input, Segmented, Sidebar, Topbar, LiveOpBanner, Toast, ConfirmModal, CmdK });