import React, { useState, useEffect, useCallback } from 'react'; import { useConfirm } from '../contexts/ConfirmContext.tsx'; import { Users, Building2, Activity, DollarSign, ShieldCheck, RefreshCw, CheckCircle2, XCircle, Clock, Search, ChevronLeft, ChevronRight, TrendingUp, Stethoscope, FlaskConical, User, ToggleLeft, ToggleRight, Save, AlertTriangle, Wifi, WifiOff, Briefcase, Trash2, Bell, Send, MapPin, Phone, MoreVertical, X, ArrowLeft, UserX, Sparkles, DoorOpen, UserSearch } from 'lucide-react'; const apiFetch = (url: string, opts: RequestInit = {}) => { const token = localStorage.getItem('SCOREODONTO_AUTH_TOKEN'); return fetch(url, { ...opts, headers: { 'Content-Type': 'application/json', ...(token ? { Authorization: `Bearer ${token}` } : {}), ...opts.headers } }); }; const ROLE_LABELS: Record = { donoconsultorio: { label: 'Consultório', color: 'emerald', icon: Briefcase }, donoclinica: { label: 'Clínica', color: 'blue', icon: Building2 }, donosala: { label: 'Dono de Sala', color: 'teal', icon: DoorOpen }, dentista: { label: 'Dentista', color: 'indigo', icon: Stethoscope }, protetico: { label: 'Protético', color: 'violet', icon: FlaskConical }, paciente: { label: 'Paciente', color: 'gray', icon: User }, superadmin: { label: 'Super Admin', color: 'red', icon: ShieldCheck }, }; const RoleBadge: React.FC<{ role: string }> = ({ role }) => { const cfg = ROLE_LABELS[role] || { label: role, color: 'gray', icon: User }; const Icon = cfg.icon; const colors: Record = { emerald: 'bg-emerald-50 text-emerald-700 border-emerald-200', blue: 'bg-blue-50 text-blue-700 border-blue-200', indigo: 'bg-indigo-50 text-indigo-700 border-indigo-200', violet: 'bg-violet-50 text-violet-700 border-violet-200', gray: 'bg-gray-100 text-gray-600 border-gray-200', red: 'bg-red-50 text-red-700 border-red-200', }; return ( {cfg.label} ); }; const StatusBadge: React.FC<{ ok: boolean }> = ({ ok }) => ok ? Ativo : Suspenso; const ASSBadge: React.FC<{ status?: string; validade?: string }> = ({ status, validade }) => { const expired = validade ? new Date(validade) < new Date() : true; if (status === 'pago' && !expired) return Pago; if (status === 'pago' && expired) return Vencido; return Pendente; }; const fmt = (d: string | null | undefined) => d ? new Date(d).toLocaleDateString('pt-BR', { day:'2-digit', month:'2-digit', year:'2-digit', hour:'2-digit', minute:'2-digit' }) : '—'; const fmtDate = (d: string | null | undefined) => d ? new Date(d).toLocaleDateString('pt-BR') : '—'; /* ─── KPI Card ─── */ const KPI: React.FC<{ icon: React.ElementType; label: string; value: string | number; sub?: string; color?: string }> = ({ icon: Icon, label, value, sub, color = 'blue' }) => { const bg: Record = { blue: 'bg-blue-600', green: 'bg-emerald-500', orange: 'bg-orange-500', violet: 'bg-violet-600' }; return (

{label}

{value}

{sub &&

{sub}

}
); }; type SuperAdminTab = 'overview' | 'users' | 'clinicas' | 'logs' | 'financeiro' | 'notificacoes'; /* ═══════════════════════════════════════════════════ MAIN VIEW */ export const SuperAdminView: React.FC<{ tab?: SuperAdminTab }> = ({ tab = 'overview' }) => { return (
{tab === 'overview' && } {tab === 'users' && } {tab === 'clinicas' && } {tab === 'logs' && } {tab === 'financeiro' && } {tab === 'notificacoes' && }
); }; /* ═══════════════════════════════════════════════════ TAB OVERVIEW */ const TabOverview: React.FC = () => { const [stats, setStats] = useState(null); const [loading, setLoading] = useState(true); useEffect(() => { apiFetch('/api/superadmin/stats') .then(r => r.ok ? r.json() : null) .then(d => { if (d?.usuarios) setStats(d); }) .finally(() => setLoading(false)); }, []); if (loading) return ; if (!stats?.usuarios) return ; const totalUsers = stats.usuarios.reduce((s: number, r: any) => s + parseInt(r.total), 0); return (
Resumo do Sistema
Usuários por Perfil
{stats.usuarios.map((r: any) => { const cfg = ROLE_LABELS[r.role] || { label: r.role, icon: User }; const Icon = cfg.icon; return (
{cfg.label}

{r.total}

{parseInt(r.suspensos) > 0 && (

{r.suspensos} suspenso(s)

)}
); })}
); }; /* ═══════════════════════════════════════════════════ TAB USERS */ const TabUsers: React.FC = () => { const [clinicas, setClinicas] = useState([]); const [loadingClinicas, setLoadingClinicas] = useState(true); const [clinicaSearch, setClinicaSearch] = useState(''); const [selectedClinica, setSelectedClinica] = useState(null); useEffect(() => { apiFetch('/api/superadmin/clinicas').then(r => r.json()).then(d => { setClinicas(Array.isArray(d) ? d : []); }).finally(() => setLoadingClinicas(false)); }, []); const filteredClinicas = clinicas.filter(c => !clinicaSearch || c.nome_fantasia?.toLowerCase().includes(clinicaSearch.toLowerCase()) || c.cidade?.toLowerCase().includes(clinicaSearch.toLowerCase()) ); /* ── Estágio 1: grid de clínicas ── */ if (!selectedClinica) { return (
setClinicaSearch(e.target.value)} className="w-full pl-9 pr-3 py-2.5 bg-white border border-gray-200 rounded-xl text-sm font-medium outline-none focus:border-blue-400 focus:ring-2 focus:ring-blue-50 transition-all" placeholder="Buscar clínica ou consultório..." />
{loadingClinicas ? : (
{filteredClinicas.map(c => { const isCons = c.dono_role === 'donoconsultorio'; return ( ); })} {/* Card: sem vínculo */}
)}
); } /* ── Estágio 2: usuários da clínica selecionada ── */ return ( setSelectedClinica(null)} /> ); }; /* ── Usuários de uma clínica específica ── */ const ClinicaUsuarios: React.FC<{ clinica: any; onBack: () => void }> = ({ clinica, onBack }) => { const confirm = useConfirm(); const [data, setData] = useState([]); const [total, setTotal] = useState(0); const [page, setPage] = useState(1); const [search, setSearch] = useState(''); const [loading, setLoading] = useState(false); const [togglingId, setTogglingId] = useState(null); const [assinaturaModal, setAssinaturaModal] = useState(null); const [menuOpen, setMenuOpen] = useState(null); const PAGE_SIZE = 20; const load = useCallback(async () => { setLoading(true); try { const params = new URLSearchParams({ page: String(page), pageSize: String(PAGE_SIZE), clinicaId: clinica.id }); if (search) params.set('search', search); const res = await apiFetch(`/api/superadmin/usuarios?${params}`); const json = await res.json(); setData(json.data || []); setTotal(json.total || 0); } finally { setLoading(false); } }, [page, search, clinica.id]); useEffect(() => { load(); }, [load]); const toggleStatus = async (u: any) => { setTogglingId(u.id); setMenuOpen(null); await apiFetch(`/api/superadmin/usuarios/${u.id}/status`, { method: 'PUT', body: JSON.stringify({ ativo: !u.ativo }) }); setTogglingId(null); load(); }; const deleteUser = async (u: any) => { setMenuOpen(null); if (!await confirm(`EXCLUIR "${u.nome}" PERMANENTEMENTE?`)) return; await apiFetch(`/api/superadmin/usuarios/${u.id}`, { method: 'DELETE' }); load(); }; const totalPages = Math.ceil(total / PAGE_SIZE); const isCons = clinica.dono_role === 'donoconsultorio'; return (
{/* Breadcrumb header */}
{clinica._none ? : isCons ? : }

{clinica.nome_fantasia}

{!clinica._none && (

{clinica.cidade ? `${clinica.cidade}${clinica.estado ? `/${clinica.estado}` : ''}` : ''} {clinica.dono_nome ? ` · ${clinica.dono_nome}` : ''}

)}
{!loading && ( {total} {total === 1 ? 'membro' : 'membros'} )}
{/* Search + refresh */}
{ setSearch(e.target.value); setPage(1); }} className="w-full pl-9 pr-3 py-2.5 bg-white border border-gray-200 rounded-xl text-sm font-medium outline-none focus:border-blue-400 focus:ring-2 focus:ring-blue-50 transition-all" placeholder="Buscar por nome ou e-mail..." />
{/* Mobile: Cards */}
{loading && } {!loading && data.length === 0 && (

Nenhum usuário encontrado

)} {data.map(u => (

{u.nome}

{u.email}

{menuOpen === u.id && (
)}
{u.assinatura_validade && (

Validade: {fmtDate(u.assinatura_validade)}

)} {u.ultimo_acesso && (

Último acesso: {fmt(u.ultimo_acesso)}

)}
))}
{/* Desktop: Table */}
{['Usuário', 'Perfil', 'Status', 'Assinatura', 'Validade', 'Último acesso', 'Ações'].map(h => ( ))} {loading && } {!loading && data.length === 0 && ( )} {data.map(u => ( ))}
{h}
Nenhum usuário nesta clínica

{u.nome}

{u.email}

{fmtDate(u.assinatura_validade)} {fmt(u.ultimo_acesso)}
{totalPages > 1 && (
{total} usuários · pág. {page}/{totalPages}
)}
{/* Mobile pagination */} {totalPages > 1 && (
{total} usuários · pág. {page}/{totalPages}
)} {assinaturaModal && { setAssinaturaModal(null); load(); }} />} {menuOpen &&
setMenuOpen(null)} />}
); }; /* ── Assinatura Modal ── */ const AssinaturaModal: React.FC<{ user: any; onClose: () => void }> = ({ user, onClose }) => { const [status, setStatus] = useState('pago'); const [meses, setMeses] = useState(1); const [valor, setValor] = useState(''); const [saving, setSaving] = useState(false); const save = async () => { setSaving(true); await apiFetch(`/api/superadmin/assinaturas/${user.id}`, { method: 'PUT', body: JSON.stringify({ status, meses, valor: parseFloat(valor) || 0 }) }); setSaving(false); onClose(); }; return (
e.stopPropagation()}>

Registrar Pagamento

{user.nome} · {user.email}

setMeses(parseInt(e.target.value))} className="w-full px-3 py-3 bg-gray-50 border border-gray-200 rounded-xl text-sm font-bold outline-none focus:border-blue-400 transition-all" />
setValor(e.target.value)} placeholder="0,00" className="w-full px-3 py-3 bg-gray-50 border border-gray-200 rounded-xl text-sm font-bold outline-none focus:border-blue-400 transition-all" />
); }; /* ═══════════════════════════════════════════════════ TAB CLINICAS */ const TabClinicas: React.FC = () => { const confirm = useConfirm(); const [data, setData] = useState([]); const [search, setSearch] = useState(''); const [estado, setEstado] = useState(''); const [loading, setLoading] = useState(false); const ESTADOS = ['AC','AL','AM','AP','BA','CE','DF','ES','GO','MA','MG','MS','MT','PA','PB','PE','PI','PR','RJ','RN','RO','RR','RS','SC','SE','SP','TO']; const load = useCallback(async () => { setLoading(true); try { const params = new URLSearchParams(); if (search) params.set('search', search); if (estado) params.set('estado', estado); const res = await apiFetch(`/api/superadmin/clinicas?${params}`); const d = await res.json(); setData(Array.isArray(d) ? d : []); } finally { setLoading(false); } }, [search, estado]); useEffect(() => { load(); }, [load]); const deleteClinica = async (c: any) => { if (!await confirm(`EXCLUIR "${c.nome_fantasia}" PERMANENTEMENTE?`)) return; await apiFetch(`/api/superadmin/clinicas/${c.id}`, { method: 'DELETE' }); load(); }; const grouped: Record = {}; for (const c of data) { const key = c.estado || 'Não informado'; (grouped[key] ??= []).push(c); } return (
setSearch(e.target.value)} className="w-full pl-9 pr-3 py-2.5 bg-white border border-gray-200 rounded-xl text-sm font-medium outline-none focus:border-blue-400 focus:ring-2 focus:ring-blue-50 transition-all" placeholder="Buscar clínica..." />
{loading ? : Object.keys(grouped).sort().map(est => (
{est} {grouped[est].length}
{/* Mobile: cards */}
{grouped[est].map(c => (

{c.nome_fantasia}

{c.documento || '—'}

{c.dono_nome && {c.dono_nome}} {c.cidade && {c.cidade}} {c.membros} membros
))}
{/* Desktop: table */}
{['Clínica', 'Dono', 'Membros', 'Cidade', 'Criada em', ''].map(h => ( ))} {grouped[est].map(c => ( ))}
{h}

{c.nome_fantasia}

{c.documento || '—'}

{c.dono_nome || '—'}

{c.dono_email || ''}

{c.membros} {c.cidade || '—'} {fmtDate(c.createdat)}
))}
); }; /* ═══════════════════════════════════════════════════ TAB LOGS */ const TabLogs: React.FC = () => { const [data, setData] = useState([]); const [total, setTotal] = useState(0); const [page, setPage] = useState(1); const [loading, setLoading] = useState(false); const PAGE_SIZE = 50; const load = useCallback(async () => { setLoading(true); try { const res = await apiFetch(`/api/superadmin/login-log?page=${page}&pageSize=${PAGE_SIZE}`); const json = await res.json(); setData(json.data || []); setTotal(json.total || 0); } finally { setLoading(false); } }, [page]); useEffect(() => { load(); }, [load]); const totalPages = Math.ceil(total / PAGE_SIZE); const statusCfg: Record = { ok: { bg: 'text-green-600 bg-green-50 border-green-200', icon: Wifi, label: 'Sucesso', dot: 'bg-green-500' }, falha: { bg: 'text-red-600 bg-red-50 border-red-200', icon: WifiOff, label: 'Falha', dot: 'bg-red-500' }, suspenso: { bg: 'text-orange-600 bg-orange-50 border-orange-200', icon: XCircle, label: 'Suspenso', dot: 'bg-orange-500' }, }; return (
Log de Acessos — {total} registros
{/* Mobile: timeline cards */}
{loading && } {data.map(l => { const cfg = statusCfg[l.status] || statusCfg.falha; return (

{l.nome || l.email}

{l.nome &&

{l.email}

}
{cfg.label}
{fmt(l.created_at)} {l.role && } {l.ip}
); })}
{/* Desktop: table */}
{['Data / Hora', 'Usuário', 'Perfil', 'IP', 'Status'].map(h => ( ))} {loading && } {data.map(l => { const cfg = statusCfg[l.status] || statusCfg.falha; const Icon = cfg.icon; return ( ); })}
{h}
{fmt(l.created_at)}

{l.nome || '—'}

{l.email}

{l.role ? : } {l.ip} {cfg.label}
{totalPages > 1 && (
pág. {page}/{totalPages}
)}
{/* Mobile pagination */} {totalPages > 1 && (
pág. {page}/{totalPages}
)}
); }; /* ═══════════════════════════════════════════════════ TAB FINANCEIRO */ const TabFinanceiro: React.FC = () => { const [pricing, setPricing] = useState>({ donoconsultorio: 49.90, donoclinica: 499.00, dentista: 49.90, biomedico: 49.90, protetico: 49.90, paciente: 0, 'plugin:locacao-salas': 0, 'plugin:profissionais-marketplace': 0 }); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [saved, setSaved] = useState(false); useEffect(() => { apiFetch('/api/superadmin/pricing').then(r => r.json()).then(d => { setPricing(d); setLoading(false); }); }, []); const save = async () => { setSaving(true); await apiFetch('/api/superadmin/pricing', { method: 'PUT', body: JSON.stringify(pricing) }); setSaving(false); setSaved(true); setTimeout(() => setSaved(false), 2500); }; const pricingRows = [ { role: 'donoconsultorio', label: 'Consultório', sub: '1 cadeira / solo', icon: Briefcase, color: 'emerald' }, { role: 'donoclinica', label: 'Clínica', sub: 'multi-cadeira', icon: Building2, color: 'blue' }, { role: 'dentista', label: 'Dentista', sub: 'por usuário/mês', icon: Stethoscope, color: 'indigo' }, { role: 'biomedico', label: 'Biomédico(a)',sub: 'por usuário/mês', icon: Sparkles, color: 'pink' }, { role: 'protetico', label: 'Protético', sub: 'por usuário/mês', icon: FlaskConical, color: 'violet' }, { role: 'paciente', label: 'Paciente', sub: '0 = gratuito', icon: User, color: 'gray' }, ]; // Add-ons: mesma tabela (settings.pricing), chave `plugin:`. const pluginRows = [ { role: 'plugin:locacao-salas', label: 'Locação de Salas', sub: 'add-on por conta/mês', icon: DoorOpen, color: 'teal' }, { role: 'plugin:profissionais-marketplace', label: 'Profissionais', sub: 'add-on por conta/mês', icon: UserSearch, color: 'orange' }, ]; return (
Tabela de Preços

Configure o valor mensal por perfil.

{loading ? : (
{(() => { const colors: Record = { emerald: 'bg-emerald-50 text-emerald-600', blue: 'bg-blue-50 text-blue-600', indigo: 'bg-indigo-50 text-indigo-600', pink: 'bg-pink-50 text-pink-600', violet: 'bg-violet-50 text-violet-600', gray: 'bg-gray-100 text-gray-500', teal: 'bg-teal-50 text-teal-600', orange: 'bg-orange-50 text-orange-600', }; const renderRow = ({ role, label, sub, icon: Icon, color }: typeof pricingRows[number]) => (

{label}

{sub}

R$ setPricing(p => ({ ...p, [role]: parseFloat(e.target.value) || 0 }))} className="w-20 sm:w-24 px-2 sm:px-3 py-2 bg-gray-50 border border-gray-200 rounded-xl text-sm font-black text-right outline-none focus:border-blue-400 focus:ring-2 focus:ring-blue-50 transition-all" /> /mês
); return ( <>
{pricingRows.map(renderRow)}

Plugins (add-ons) · 0 = incluído no plano

{pluginRows.map(renderRow)}
); })()}

Alterações entram em vigor no próximo ciclo.

)}

Como funciona

  • • Registre o pagamento na aba Usuários → botão Pag.
  • • Defina quantos meses o acesso ficará liberado (1–12)
  • • Ao suspender, o usuário não consegue mais fazer login
  • • Usuários vencidos aparecem como Pendente
); }; /* ═══════════════════════════════════════════════════ TAB NOTIFICACOES */ const TabNotificacoes: React.FC = () => { const [titulo, setTitulo] = useState(''); const [mensagem, setMensagem] = useState(''); const [tipo, setTipo] = useState<'info' | 'sucesso' | 'alerta' | 'erro'>('info'); const [targetEmail, setTargetEmail] = useState(''); const [sending, setSending] = useState(false); const [sent, setSent] = useState(false); const [error, setError] = useState(''); const [history, setHistory] = useState([]); const [loadingH, setLoadingH] = useState(true); const loadHistory = async () => { setLoadingH(true); try { const res = await apiFetch('/api/superadmin/notificacoes-history'); if (res.ok) setHistory(await res.json()); } finally { setLoadingH(false); } }; useEffect(() => { loadHistory(); }, []); const resolveTarget = async (): Promise => { if (!targetEmail.trim()) return null; const res = await apiFetch(`/api/superadmin/usuarios?search=${encodeURIComponent(targetEmail.trim())}&pageSize=1`); const json = await res.json(); const user = json.data?.[0]; if (!user) { setError(`Usuário "${targetEmail}" não encontrado.`); return 'NOT_FOUND'; } return user.id; }; const handleSend = async () => { if (!titulo.trim() || !mensagem.trim()) { setError('Preencha título e mensagem.'); return; } setSending(true); setError(''); try { let usuario_id: string | null | undefined = undefined; if (targetEmail.trim()) { const resolved = await resolveTarget(); if (resolved === 'NOT_FOUND') { setSending(false); return; } usuario_id = resolved; } const res = await apiFetch('/api/superadmin/notificacoes', { method: 'POST', body: JSON.stringify({ titulo: titulo.trim(), mensagem: mensagem.trim(), tipo, usuario_id }), }); if (res.ok) { setSent(true); setTitulo(''); setMensagem(''); setTargetEmail(''); setTimeout(() => setSent(false), 3000); loadHistory(); } else { const d = await res.json(); setError(d.error || 'Erro ao enviar.'); } } catch { setError('Falha na conexão.'); } finally { setSending(false); } }; const TIPO_COLORS: Record = { info: 'bg-blue-50 text-blue-700 border-blue-200', sucesso: 'bg-emerald-50 text-emerald-700 border-emerald-200', alerta: 'bg-yellow-50 text-yellow-700 border-yellow-200', erro: 'bg-red-50 text-red-700 border-red-200', }; return (
Enviar Notificação

Envie avisos globais ou para um usuário específico.

setTargetEmail(e.target.value)} placeholder="E-mail do usuário — vazio = todos" className="w-full pl-9 border border-gray-200 rounded-xl px-4 py-2.5 text-sm font-medium text-gray-800 focus:ring-2 focus:ring-blue-100 focus:border-blue-300 outline-none transition-all" />
{(['info', 'sucesso', 'alerta', 'erro'] as const).map(t => ( ))}
setTitulo(e.target.value)} placeholder="Ex: Manutenção programada" className="w-full border border-gray-200 rounded-xl px-4 py-2.5 text-sm font-medium text-gray-800 focus:ring-2 focus:ring-blue-100 focus:border-blue-300 outline-none transition-all" />