const TENANT_STATUSES = [ { key: 'active', label: 'Ativo', tone: 'emerald' }, { key: 'trial', label: 'Trial', tone: 'amber' }, { key: 'suspended', label: 'Suspenso', tone: 'coral' }, ]; function ScreenTenants() { const [tenants, setTenants] = useState([]); const [loading, setLoading] = useState(true); const [err, setErr] = useState(null); const [createModal, setCreateModal] = useState(false); useEffect(() => { loadTenants(); }, []); const loadTenants = async () => { setLoading(true); setErr(null); try { const data = await window.scpDb.tenants.list(); setTenants(data); } catch (e) { setErr(e.message || 'Falha ao carregar assinantes'); } finally { setLoading(false); } }; const statusTone = (s) => TENANT_STATUSES.find(x => x.key === s)?.tone || 'fg'; const statusLabel = (s) => TENANT_STATUSES.find(x => x.key === s)?.label || s; const counts = tenants.reduce((acc, t) => ({ ...acc, [t.status]: (acc[t.status] || 0) + 1 }), {}); const handleDelete = async (t) => { const msg = `APAGAR DEFINITIVAMENTE o assinante "${t.name}"?\n\nIsso apaga TODOS os dados dele: produtos, fornecedores, cotações, pedidos, estoque, usuários. Os usuários do auth ficam órfãos (você precisa apagar manualmente).\n\nEsta ação NÃO pode ser desfeita. Para confirmar, digite o nome exato da empresa abaixo:`; const typed = window.prompt(msg, ''); if (typed === null) return; if (typed.trim() !== t.name) { window.scpToast('Nome não confere — exclusão cancelada', { kind: 'amber' }); return; } try { await window.scpDb.tenants.remove(t.id); await loadTenants(); window.scpToast('Assinante apagado', { kind: 'amber', sub: t.name }); } catch (e) { window.scpToast('Erro ao apagar', { kind: 'coral', sub: e.message }); } }; const handleEditNotes = async (t) => { const current = t.notes || ''; const next = window.prompt(`Notas internas de "${t.name}":\n\nEdite abaixo (Ctrl+Enter ou Enter pra salvar, Cancelar pra desistir).`, current); if (next === null) return; // cancelou if (next === current) return; // sem mudança try { await window.scpDb.tenants.update(t.id, { notes: next.trim() || null }); await loadTenants(); window.scpToast('Notas atualizadas', { kind: 'emerald', sub: t.name }); } catch (e) { window.scpToast('Erro ao salvar notas', { kind: 'coral', sub: e.message }); } }; const handleSetStatus = async (t, newStatus) => { const verb = newStatus === 'active' ? 'reativar' : 'suspender'; if (!confirm(`Deseja ${verb} "${t.name}"?`)) return; try { await window.scpDb.tenants.setStatus(t.id, newStatus); await loadTenants(); window.scpToast(`Assinante ${newStatus === 'active' ? 'reativado' : 'suspenso'}`, { kind: newStatus === 'active' ? 'emerald' : 'amber', sub: t.name }); } catch (e) { window.scpToast('Erro ao mudar status', { kind: 'coral', sub: e.message }); } }; return (

Assinantes

{counts.active || 0} ativos · {counts.trial || 0} trial · {counts.suspended || 0} suspensos

} onClick={() => setCreateModal(true)}> Novo assinante
{err && ( Falha:{' '} {err} )} {loading && } {!loading && tenants.length === 0 && ( )} {!loading && tenants.map(t => ( ))}
EmpresaCNPJStatusUsuários Último acessoCriado emNotas
Carregando…
Nenhum assinante cadastrado. Clique em "Novo assinante" para começar.
{t.name}
{t.city || '—'}
{t.cnpj || '—'} {statusLabel(t.status)} {t.users} {t.last_seen_at ? new Date(t.last_seen_at).toLocaleString('pt-BR') : 'nunca'} {new Date(t.created_at).toLocaleDateString('pt-BR')} {t.notes ? (t.notes.length > 40 ? t.notes.slice(0, 40) + '…' : t.notes) : }
handleEditNotes(t)} title="Editar notas">Notas {t.status !== 'suspended' && ( handleSetStatus(t, 'suspended')}>Suspender )} {t.status === 'suspended' && ( handleSetStatus(t, 'active')}>Reativar )} handleDelete(t)} title="Apagar definitivamente" style={{ color: 'oklch(0.82 0.14 25)' }}>
{createModal && ( setCreateModal(false)} onCreated={async () => { setCreateModal(false); await loadTenants(); }} /> )}
); } function NewTenantModal({ onClose, onCreated }) { const [name, setName] = useState(''); const [cnpj, setCnpj] = useState(''); const [email, setEmail] = useState(''); const [city, setCity] = useState(''); const [address, setAddress] = useState(''); const [status, setStatus] = useState('trial'); const [notes, setNotes] = useState(''); const [adminName, setAdminName] = useState(''); const [adminEmail, setAdminEmail] = useState(''); const [adminPwd, setAdminPwd] = useState(generateTempPassword()); const [saving, setSaving] = useState(false); const emailRe = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; const canSubmit = !saving && name.trim().length >= 2 && adminName.trim().length >= 2 && emailRe.test(adminEmail.trim()) && adminPwd.length >= 8; const submit = async () => { if (!canSubmit) return; setSaving(true); try { const result = await window.scpDb.tenants.create({ company: { name: name.trim(), cnpj: cnpj.trim(), email: email.trim(), city: city.trim(), address: address.trim(), status, notes: notes.trim() || null }, admin: { name: adminName.trim(), email: adminEmail.trim().toLowerCase(), password: adminPwd }, }); const sub = result.needsConfirmation ? `Admin precisa confirmar e-mail. Senha temporária: ${adminPwd}` : `Senha temporária do admin: ${adminPwd}`; window.scpToast(`Assinante criado · ${result.company.name}`, { kind: 'emerald', sub }); await onCreated(); } catch (e) { window.scpToast('Erro ao criar', { kind: 'coral', sub: e.message }); } finally { setSaving(false); } }; return ReactDOM.createPortal(
e.stopPropagation()}>

Novo assinante

} size="sm" onClick={onClose}/>

Dados da empresa