feat: implement real tenant creation and listing
All checks were successful
Build and Deploy / build-and-push (push) Successful in 2m34s

This commit is contained in:
Cauê Faleiros
2026-02-24 11:44:28 -03:00
parent 113ea4abfb
commit 2742bafb00
3 changed files with 108 additions and 8 deletions

View File

@@ -123,13 +123,56 @@ app.get('/api/attendances/:id', async (req, res) => {
// --- Rotas de Tenants (Super Admin) --- // --- Rotas de Tenants (Super Admin) ---
app.get('/api/tenants', async (req, res) => { app.get('/api/tenants', async (req, res) => {
try { try {
const [rows] = await pool.query('SELECT * FROM tenants'); // Buscar tenants e contar usuários/atendimentos
const query = `
SELECT t.*,
(SELECT COUNT(*) FROM users u WHERE u.tenant_id = t.id) as user_count,
(SELECT COUNT(*) FROM attendances a WHERE a.tenant_id = t.id) as attendance_count
FROM tenants t
`;
const [rows] = await pool.query(query);
res.json(rows); res.json(rows);
} catch (error) { } catch (error) {
res.status(500).json({ error: error.message }); res.status(500).json({ error: error.message });
} }
}); });
// Criar Tenant
app.post('/api/tenants', async (req, res) => {
const { name, slug, admin_email, status } = req.body;
const crypto = require('crypto');
const connection = await pool.getConnection();
try {
await connection.beginTransaction();
const tenantId = `tenant_${crypto.randomUUID().split('-')[0]}`; // Simple ID generation
// 1. Criar Tenant
await connection.query(
'INSERT INTO tenants (id, name, slug, admin_email, status) VALUES (?, ?, ?, ?, ?)',
[tenantId, name, slug, admin_email, status || 'active']
);
// 2. Criar Usuário Admin Default
const userId = `u_${crypto.randomUUID().split('-')[0]}`;
await connection.query(
'INSERT INTO users (id, tenant_id, name, email, role, status) VALUES (?, ?, ?, ?, ?, ?)',
[userId, tenantId, 'Admin', admin_email, 'admin', 'active']
);
await connection.commit();
res.status(201).json({ message: 'Tenant created successfully', id: tenantId });
} catch (error) {
await connection.rollback();
console.error('Erro ao criar tenant:', error);
res.status(500).json({ error: error.message });
} finally {
connection.release();
}
});
// Serve index.html for any unknown routes (for client-side routing) // Serve index.html for any unknown routes (for client-side routing)
if (process.env.NODE_ENV === 'production') { if (process.env.NODE_ENV === 'production') {
app.get('*', (req, res) => { app.get('*', (req, res) => {

View File

@@ -3,7 +3,7 @@ import {
Building2, Users, MessageSquare, Plus, Search, MoreHorizontal, Building2, Users, MessageSquare, Plus, Search, MoreHorizontal,
Edit, Trash2, Shield, Calendar, ChevronDown, ChevronUp, ChevronsUpDown, X Edit, Trash2, Shield, Calendar, ChevronDown, ChevronUp, ChevronsUpDown, X
} from 'lucide-react'; } from 'lucide-react';
import { TENANTS, MOCK_ATTENDANCES, USERS } from '../constants'; import { getTenants, createTenant } from '../services/dataService';
import { Tenant } from '../types'; import { Tenant } from '../types';
import { DateRangePicker } from '../components/DateRangePicker'; import { DateRangePicker } from '../components/DateRangePicker';
import { KPICard } from '../components/KPICard'; import { KPICard } from '../components/KPICard';
@@ -18,7 +18,8 @@ export const SuperAdmin: React.FC = () => {
}); });
const [selectedTenantId, setSelectedTenantId] = useState<string>('all'); const [selectedTenantId, setSelectedTenantId] = useState<string>('all');
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState('');
const [tenants, setTenants] = useState<Tenant[]>(TENANTS); const [tenants, setTenants] = useState<Tenant[]>([]);
const [loading, setLoading] = useState(true);
const [isModalOpen, setIsModalOpen] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false);
const [editingTenant, setEditingTenant] = useState<Tenant | null>(null); const [editingTenant, setEditingTenant] = useState<Tenant | null>(null);
@@ -26,6 +27,18 @@ export const SuperAdmin: React.FC = () => {
const [sortKey, setSortKey] = useState<keyof Tenant>('created_at'); const [sortKey, setSortKey] = useState<keyof Tenant>('created_at');
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('desc'); const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('desc');
// Load Tenants from API
const loadTenants = async () => {
setLoading(true);
const data = await getTenants();
setTenants(data);
setLoading(false);
};
React.useEffect(() => {
loadTenants();
}, []);
// --- Metrics --- // --- Metrics ---
const totalTenants = tenants.length; const totalTenants = tenants.length;
// Mock aggregation for demo // Mock aggregation for demo
@@ -82,16 +95,31 @@ export const SuperAdmin: React.FC = () => {
const handleDelete = (id: string) => { const handleDelete = (id: string) => {
if (confirm('Tem certeza que deseja excluir esta organização? Esta ação não pode ser desfeita.')) { if (confirm('Tem certeza que deseja excluir esta organização? Esta ação não pode ser desfeita.')) {
// TODO: Implement delete API
setTenants(prev => prev.filter(t => t.id !== id)); setTenants(prev => prev.filter(t => t.id !== id));
} }
}; };
const handleSaveTenant = (e: React.FormEvent) => { const handleSaveTenant = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
// Logic to save (mock) const form = e.target as HTMLFormElement;
setIsModalOpen(false);
setEditingTenant(null); // Extract values manually or use state. For simplicity using form elements
alert('Organização salva com sucesso (Mock)'); const name = (form.elements.namedItem('name') as HTMLInputElement).value;
const slug = (form.elements.namedItem('slug') as HTMLInputElement).value;
const admin_email = (form.elements.namedItem('admin_email') as HTMLInputElement).value;
const status = (form.elements.namedItem('status') as HTMLSelectElement).value;
const success = await createTenant({ name, slug, admin_email, status });
if (success) {
setIsModalOpen(false);
setEditingTenant(null);
loadTenants(); // Reload list
alert('Organização salva com sucesso!');
} else {
alert('Erro ao salvar organização. Verifique o console.');
}
}; };
// --- Helper Components --- // --- Helper Components ---
@@ -297,6 +325,7 @@ export const SuperAdmin: React.FC = () => {
<label className="text-sm font-medium text-slate-700">Nome da Organização</label> <label className="text-sm font-medium text-slate-700">Nome da Organização</label>
<input <input
type="text" type="text"
name="name"
defaultValue={editingTenant?.name} defaultValue={editingTenant?.name}
className="w-full px-3 py-2 bg-slate-50 border border-slate-200 rounded-lg text-sm focus:ring-2 focus:ring-blue-100 outline-none" className="w-full px-3 py-2 bg-slate-50 border border-slate-200 rounded-lg text-sm focus:ring-2 focus:ring-blue-100 outline-none"
placeholder="ex. Acme Corp" placeholder="ex. Acme Corp"
@@ -309,6 +338,7 @@ export const SuperAdmin: React.FC = () => {
<label className="text-sm font-medium text-slate-700">Slug</label> <label className="text-sm font-medium text-slate-700">Slug</label>
<input <input
type="text" type="text"
name="slug"
defaultValue={editingTenant?.slug} defaultValue={editingTenant?.slug}
className="w-full px-3 py-2 bg-slate-50 border border-slate-200 rounded-lg text-sm focus:ring-2 focus:ring-blue-100 outline-none" className="w-full px-3 py-2 bg-slate-50 border border-slate-200 rounded-lg text-sm focus:ring-2 focus:ring-blue-100 outline-none"
placeholder="ex. acme-corp" placeholder="ex. acme-corp"
@@ -317,6 +347,7 @@ export const SuperAdmin: React.FC = () => {
<div className="space-y-2"> <div className="space-y-2">
<label className="text-sm font-medium text-slate-700">Status</label> <label className="text-sm font-medium text-slate-700">Status</label>
<select <select
name="status"
defaultValue={editingTenant?.status || 'active'} defaultValue={editingTenant?.status || 'active'}
className="w-full px-3 py-2 bg-slate-50 border border-slate-200 rounded-lg text-sm focus:ring-2 focus:ring-blue-100 outline-none" className="w-full px-3 py-2 bg-slate-50 border border-slate-200 rounded-lg text-sm focus:ring-2 focus:ring-blue-100 outline-none"
> >
@@ -331,6 +362,7 @@ export const SuperAdmin: React.FC = () => {
<label className="text-sm font-medium text-slate-700">E-mail do Admin</label> <label className="text-sm font-medium text-slate-700">E-mail do Admin</label>
<input <input
type="email" type="email"
name="admin_email"
defaultValue={editingTenant?.admin_email} defaultValue={editingTenant?.admin_email}
className="w-full px-3 py-2 bg-slate-50 border border-slate-200 rounded-lg text-sm focus:ring-2 focus:ring-blue-100 outline-none" className="w-full px-3 py-2 bg-slate-50 border border-slate-200 rounded-lg text-sm focus:ring-2 focus:ring-blue-100 outline-none"
placeholder="admin@empresa.com" placeholder="admin@empresa.com"

View File

@@ -67,3 +67,28 @@ export const getAttendanceById = async (id: string): Promise<Attendance | undefi
return undefined; return undefined;
} }
}; };
export const getTenants = async (): Promise<any[]> => {
try {
const response = await fetch(`${API_URL}/tenants`);
if (!response.ok) throw new Error('Falha ao buscar tenants');
return await response.json();
} catch (error) {
console.error("API Error (getTenants):", error);
return [];
}
};
export const createTenant = async (tenantData: any): Promise<boolean> => {
try {
const response = await fetch(`${API_URL}/tenants`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(tenantData)
});
return response.ok;
} catch (error) {
console.error("API Error (createTenant):", error);
return false;
}
};