agendapro/admin/src/components/Tenants.tsx

200 lines
11 KiB
TypeScript

'use client';
import { useState, useEffect } from 'react';
import { Search, Plus, X, Edit, Trash2, AlertTriangle, Ban, CheckCircle } from 'lucide-react';
interface Tenant {
id: string; name: string; slug: string; type: string; plan: string; status: string; email: string; phone: string; mrr: number; createdAt: string; trialEnds: string; password?: string;
}
const statusMap: Record<string, { label: string; cls: string }> = {
active: { label: 'Ativo', cls: 'badge-success' }, trial: { label: 'Trial', cls: 'badge-info' },
past_due: { label: 'Inadimplente', cls: 'badge-danger' }, cancelled: { label: 'Cancelado', cls: 'badge-warning' },
suspended: { label: 'Suspenso', cls: 'badge-danger' },
};
export default function Tenants() {
const [tenants, setTenants] = useState<Tenant[]>([]);
const [search, setSearch] = useState('');
const [filterStatus, setFilterStatus] = useState('all');
const [showModal, setShowModal] = useState(false);
const [editing, setEditing] = useState<Tenant | null>(null);
const [deleteConfirm, setDeleteConfirm] = useState<string | null>(null);
const [form, setForm] = useState<Partial<Tenant>>({});
const [loading, setLoading] = useState(true);
const fetchTenants = async () => {
try {
const res = await fetch('/api/tenants');
if (res.ok) {
const data = await res.json();
setTenants(data);
}
} catch (e) {
console.error(e);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchTenants();
}, []);
const filtered = tenants.filter(t => {
const matchSearch = t.name.toLowerCase().includes(search.toLowerCase()) || (t.email && t.email.toLowerCase().includes(search.toLowerCase()));
const matchStatus = filterStatus === 'all' || t.status === filterStatus;
return matchSearch && matchStatus;
});
const openEdit = (t: Tenant) => { setEditing(t); setForm({ ...t }); setShowModal(true); };
const openAdd = () => { setEditing(null); setForm({ status: 'trial', plan: 'Starter', type: 'Barbearia' }); setShowModal(true); };
const handleSave = async () => {
if (!form.name?.trim() || !form.slug?.trim()) return;
const method = editing ? 'PUT' : 'POST';
try {
const res = await fetch('/api/tenants', {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(form)
});
if (res.ok) {
fetchTenants();
setShowModal(false);
}
} catch (e) {
console.error(e);
}
};
const handleDelete = async (id: string) => {
try {
await fetch(`/api/tenants?id=${id}`, { method: 'DELETE' });
setTenants(prev => prev.filter(t => t.id !== id));
setDeleteConfirm(null);
} catch (e) {
console.error(e);
}
};
const toggleStatus = async (t: Tenant, newStatus: string) => {
try {
const res = await fetch('/api/tenants', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ...t, status: newStatus })
});
if (res.ok) {
fetchTenants();
}
} catch (err) {
console.error(err);
}
};
if (loading) return <div className="p-8 text-center" style={{color: 'var(--text-muted)'}}>Carregando estabelecimentos...</div>;
return (
<div className="slide-up">
<div className="flex-between mb-6">
<div className="flex-gap" style={{ gap: '12px' }}>
<div style={{ position: 'relative' }}>
<Search size={18} style={{ position: 'absolute', left: 14, top: '50%', transform: 'translateY(-50%)', color: 'var(--text-muted)' }} />
<input className="form-input" placeholder="Buscar assinante..." style={{ width: 300, paddingLeft: 42 }} value={search} onChange={e => setSearch(e.target.value)} />
</div>
<select className="form-select" style={{ width: 160 }} value={filterStatus} onChange={e => setFilterStatus(e.target.value)}>
<option value="all">Todos</option>
<option value="active">Ativos</option>
<option value="trial">Em Trial</option>
<option value="past_due">Inadimplentes</option>
<option value="cancelled">Cancelados</option>
<option value="suspended">Suspensos</option>
</select>
</div>
<button className="btn btn-primary" onClick={openAdd}><Plus size={16} /> Novo Assinante</button>
</div>
<div className="card" style={{ padding: 0 }}>
<div className="table-container">
<table>
<thead><tr><th>Estabelecimento</th><th>Tipo</th><th>Plano</th><th>Status</th><th>MRR</th><th>Desde</th><th style={{ width: 120 }}>Ações</th></tr></thead>
<tbody>
{filtered.map(t => (
<tr key={t.id}>
<td><div className="flex-gap"><div className="avatar" style={{ width: 32, height: 32, fontSize: 12 }}>{t.name[0]}</div><div><div style={{ fontWeight: 600 }}>{t.name}</div><div style={{ fontSize: 11, color: 'var(--text-muted)' }}>{t.email || t.slug}</div></div></div></td>
<td style={{ fontSize: 13 }}>{t.type}</td>
<td><span className="badge badge-amber">{t.plan}</span></td>
<td><span className={`badge ${statusMap[t.status]?.cls || 'badge-secondary'}`}>{statusMap[t.status]?.label || t.status}</span></td>
<td style={{ fontWeight: 600 }}>{t.mrr > 0 ? `R$ ${t.mrr.toFixed(2)}` : '-'}</td>
<td style={{ fontSize: 13, color: 'var(--text-secondary)' }}>{t.createdAt ? new Date(t.createdAt + 'T12:00:00').toLocaleDateString('pt-BR') : '-'}</td>
<td>
{deleteConfirm === t.id ? (
<div style={{ display: 'flex', gap: 4 }}>
<button className="btn btn-sm btn-danger" onClick={() => handleDelete(t.id)}>Sim</button>
<button className="btn btn-sm btn-secondary" onClick={() => setDeleteConfirm(null)}>Não</button>
</div>
) : (
<div style={{ display: 'flex', gap: 4 }}>
<button className="btn-ghost btn-icon" title="Editar" onClick={() => openEdit(t)}><Edit size={14} /></button>
{t.status === 'active' && <button className="btn-ghost btn-icon" title="Suspender" style={{ color: 'var(--warning)' }} onClick={() => toggleStatus(t, 'suspended')}><Ban size={14} /></button>}
{t.status === 'suspended' && <button className="btn-ghost btn-icon" title="Reativar" style={{ color: 'var(--success)' }} onClick={() => toggleStatus(t, 'active')}><CheckCircle size={14} /></button>}
<button className="btn-ghost btn-icon" title="Excluir" style={{ color: 'var(--danger)' }} onClick={() => setDeleteConfirm(t.id)}><Trash2 size={14} /></button>
</div>
)}
</td>
</tr>
))}
{filtered.length === 0 && <tr><td colSpan={7} style={{textAlign: 'center', padding: '40px'}}>Nenhum assinante encontrado.</td></tr>}
</tbody>
</table>
</div>
</div>
{showModal && (
<div className="modal-overlay" onClick={() => setShowModal(false)}>
<div className="modal" onClick={e => e.stopPropagation()}>
<div className="modal-header">
<h2 className="modal-title">{editing ? 'Editar Assinante' : 'Novo Assinante'}</h2>
<button className="btn-ghost btn-icon" onClick={() => setShowModal(false)}><X size={20} /></button>
</div>
<div className="modal-body">
<div className="form-group"><label className="form-label">Nome do Estabelecimento *</label><input className="form-input" value={form.name || ''} onChange={e => setForm(p => ({ ...p, name: e.target.value }))} /></div>
<div className="grid-2">
<div className="form-group"><label className="form-label">Slug *</label><input className="form-input" value={form.slug || ''} onChange={e => setForm(p => ({ ...p, slug: e.target.value }))} disabled={!!editing} /></div>
<div className="form-group"><label className="form-label">Tipo</label>
<select className="form-select" value={form.type || ''} onChange={e => setForm(p => ({ ...p, type: e.target.value }))}>
<option value="Barbearia">Barbearia</option><option value="Manicure">Manicure</option><option value="Massagem">Massagem</option><option value="Estética">Estética</option><option value="Salão">Salão</option>
</select>
</div>
</div>
<div className="grid-2">
<div className="form-group"><label className="form-label">E-mail (Login) *</label><input type="email" required className="form-input" value={form.email || ''} onChange={e => setForm(p => ({ ...p, email: e.target.value }))} /></div>
<div className="form-group"><label className="form-label">Telefone</label><input className="form-input" value={form.phone || ''} onChange={e => setForm(p => ({ ...p, phone: e.target.value }))} /></div>
</div>
<div className="grid-2">
<div className="form-group"><label className="form-label">Senha de Acesso {editing ? '(Deixe em branco para manter)' : '*'}</label><input type="text" className="form-input" placeholder={editing ? '••••••••' : 'Senha do Inquilino'} value={form.password || ''} onChange={e => setForm(p => ({ ...p, password: e.target.value }))} /></div>
</div>
<div className="grid-2">
<div className="form-group"><label className="form-label">Plano</label>
<select className="form-select" value={form.plan || ''} onChange={e => setForm(p => ({ ...p, plan: e.target.value }))}>
<option>Starter</option><option>Professional</option><option>Business</option><option>Enterprise</option>
</select>
</div>
<div className="form-group"><label className="form-label">Status</label>
<select className="form-select" value={form.status || ''} onChange={e => setForm(p => ({ ...p, status: e.target.value }))}>
<option value="active">Ativo</option><option value="trial">Trial</option><option value="past_due">Inadimplente</option><option value="suspended">Suspenso</option><option value="cancelled">Cancelado</option>
</select>
</div>
</div>
</div>
<div className="modal-footer">
<button className="btn btn-secondary" onClick={() => setShowModal(false)}>Cancelar</button>
<button className="btn btn-primary" onClick={handleSave}>{editing ? 'Salvar' : 'Criar Assinante'}</button>
</div>
</div>
</div>
)}
</div>
);
}