diff --git a/backend/index.js b/backend/index.js
index 37a400c..1e5932f 100644
--- a/backend/index.js
+++ b/backend/index.js
@@ -457,7 +457,14 @@ apiRouter.get('/teams', async (req, res) => {
try {
const { tenantId } = req.query;
const effectiveTenantId = req.user.role === 'super_admin' ? tenantId : req.user.tenant_id;
- const [rows] = await pool.query('SELECT * FROM teams WHERE tenant_id = ?', [effectiveTenantId]);
+
+ let q = 'SELECT * FROM teams';
+ const params = [];
+ if (effectiveTenantId && effectiveTenantId !== 'all') {
+ q += ' WHERE tenant_id = ?';
+ params.push(effectiveTenantId);
+ }
+ const [rows] = await pool.query(q, params);
res.json(rows);
} catch (error) {
res.status(500).json({ error: error.message });
@@ -509,13 +516,49 @@ apiRouter.post('/tenants', requireRole(['super_admin']), async (req, res) => {
await connection.beginTransaction();
const tid = `tenant_${crypto.randomUUID().split('-')[0]}`;
await connection.query('INSERT INTO tenants (id, name, slug, admin_email, status) VALUES (?, ?, ?, ?, ?)', [tid, name, slug, admin_email, status || 'active']);
- const uid = `u_${crypto.randomUUID().split('-')[0]}`;
- await connection.query('INSERT INTO users (id, tenant_id, name, email, role) VALUES (?, ?, ?, ?, ?)', [uid, tid, 'Admin', admin_email, 'admin']);
+
+ // Check if user already exists
+ const [existingUser] = await connection.query('SELECT id FROM users WHERE email = ?', [admin_email]);
+ if (existingUser.length === 0) {
+ const uid = `u_${crypto.randomUUID().split('-')[0]}`;
+ const placeholderHash = 'pending_setup';
+ await connection.query('INSERT INTO users (id, tenant_id, name, email, password_hash, role) VALUES (?, ?, ?, ?, ?, ?)', [uid, tid, 'Admin', admin_email, placeholderHash, 'admin']);
+
+ const token = crypto.randomBytes(32).toString('hex');
+ await connection.query('INSERT INTO password_resets (email, token, expires_at) VALUES (?, ?, DATE_ADD(NOW(), INTERVAL 24 HOUR))', [admin_email, token]);
+
+ const setupLink = `${process.env.APP_URL || 'http://localhost:3001'}/#/reset-password?token=${token}`;
+ await transporter.sendMail({
+ from: `"Fasto" <${process.env.MAIL_FROM || 'nao-responda@blyzer.com.br'}>`,
+ to: admin_email,
+ subject: 'Bem-vindo ao Fasto - Crie sua senha',
+ html: `
VocĆŖ foi convidado para ser Admin. Crie sua senha aqui.
`
+ }).catch(err => console.error("Email failed:", err));
+ }
+
await connection.commit();
res.status(201).json({ id: tid });
} catch (error) { await connection.rollback(); res.status(500).json({ error: error.message }); } finally { connection.release(); }
});
+apiRouter.put('/tenants/:id', requireRole(['super_admin']), async (req, res) => {
+ const { name, slug, admin_email, status } = req.body;
+ try {
+ await pool.query(
+ 'UPDATE tenants SET name = ?, slug = ?, admin_email = ?, status = ? WHERE id = ?',
+ [name, slug || null, admin_email, status, req.params.id]
+ );
+ res.json({ message: 'Tenant updated successfully.' });
+ } catch (error) { res.status(500).json({ error: error.message }); }
+});
+
+apiRouter.delete('/tenants/:id', requireRole(['super_admin']), async (req, res) => {
+ try {
+ await pool.query('DELETE FROM tenants WHERE id = ?', [req.params.id]);
+ res.json({ message: 'Tenant deleted successfully.' });
+ } catch (error) { res.status(500).json({ error: error.message }); }
+});
+
// Mount the API Router
app.use('/api', apiRouter);
@@ -529,6 +572,55 @@ if (process.env.NODE_ENV === 'production') {
});
}
-app.listen(PORT, () => {
+// Auto-provision Super Admin
+const provisionSuperAdmin = async () => {
+ const email = 'suporte@blyzer.com.br';
+ try {
+ // Ensure system tenant exists
+ await pool.query('INSERT IGNORE INTO tenants (id, name, slug, admin_email, status) VALUES (?, ?, ?, ?, ?)', ['system', 'System Admin', 'system', email, 'active']);
+
+ const [existing] = await pool.query('SELECT id FROM users WHERE email = ?', [email]);
+ if (existing.length === 0) {
+ console.log('Provisioning default super_admin...');
+ const uid = `u_${crypto.randomUUID().split('-')[0]}`;
+ const placeholderHash = 'pending_setup';
+
+ await pool.query(
+ 'INSERT INTO users (id, tenant_id, name, email, password_hash, role, status) VALUES (?, ?, ?, ?, ?, ?, ?)',
+ [uid, 'system', 'Blyzer Suporte', email, placeholderHash, 'super_admin', 'active']
+ );
+
+ const token = crypto.randomBytes(32).toString('hex');
+ await pool.query(
+ 'INSERT INTO password_resets (email, token, expires_at) VALUES (?, ?, DATE_ADD(NOW(), INTERVAL 24 HOUR))',
+ [email, token]
+ );
+
+ const setupLink = `${process.env.APP_URL || 'http://localhost:3001'}/#/reset-password?token=${token}`;
+ console.log(`\n\n=== SUPER ADMIN SETUP LINK ===\n${setupLink}\n==============================\n\n`);
+
+ await transporter.sendMail({
+ from: `"Fasto" <${process.env.MAIL_FROM || 'nao-responda@blyzer.com.br'}>`,
+ to: email,
+ subject: 'Conta Super Admin Criada - Fasto',
+ html: `
+
+
Conta Super Admin Gerada
+
Sua conta de suporte (super_admin) foi criada no Fasto.
+
+
Este link expira em 24 horas.
+
+ `
+ }).catch(err => console.error("Failed to send super_admin email:", err));
+ }
+ } catch (error) {
+ console.error('Failed to provision super_admin:', error);
+ }
+};
+
+app.listen(PORT, async () => {
+ await provisionSuperAdmin();
console.log(`š Servidor Backend rodando em http://localhost:${PORT}`);
});
diff --git a/components/Layout.tsx b/components/Layout.tsx
index 74f91bf..2a92708 100644
--- a/components/Layout.tsx
+++ b/components/Layout.tsx
@@ -124,6 +124,7 @@ export const Layout: React.FC<{ children: React.ReactNode }> = ({ children }) =>
Super Admin
+
>
)}
diff --git a/pages/SuperAdmin.tsx b/pages/SuperAdmin.tsx
index 450fdec..0cd5c34 100644
--- a/pages/SuperAdmin.tsx
+++ b/pages/SuperAdmin.tsx
@@ -3,7 +3,7 @@ import {
Building2, Users, MessageSquare, Plus, Search,
Edit, Trash2, ChevronDown, ChevronUp, ChevronsUpDown, X
} from 'lucide-react';
-import { getTenants, createTenant } from '../services/dataService';
+import { getTenants, createTenant, deleteTenant } from '../services/dataService';
import { Tenant } from '../types';
import { DateRangePicker } from '../components/DateRangePicker';
import { KPICard } from '../components/KPICard';
@@ -76,9 +76,18 @@ export const SuperAdmin: React.FC = () => {
setIsModalOpen(true);
};
- const handleDelete = (id: string) => {
+ const handleDelete = async (id: string) => {
+ if (id === 'system') {
+ alert('A organização do sistema nĆ£o pode ser excluĆda.');
+ return;
+ }
if (confirm('Tem certeza que deseja excluir esta organização? Esta ação não pode ser desfeita.')) {
- setTenants(prev => prev.filter(t => t.id !== id));
+ const success = await deleteTenant(id);
+ if (success) {
+ setTenants(prev => prev.filter(t => t.id !== id));
+ } else {
+ alert('Erro ao excluir a organização do servidor.');
+ }
}
};
diff --git a/pages/TeamManagement.tsx b/pages/TeamManagement.tsx
index 00d5bdb..1689302 100644
--- a/pages/TeamManagement.tsx
+++ b/pages/TeamManagement.tsx
@@ -1,11 +1,12 @@
import React, { useState, useEffect } from 'react';
import { Users, Plus, Mail, Search, X, Edit, Trash2, Loader2, AlertTriangle } from 'lucide-react';
-import { getUsers, getTeams, createMember, deleteUser, updateUser, getUserById } from '../services/dataService';
-import { User } from '../types';
+import { getUsers, getTeams, createMember, deleteUser, updateUser, getUserById, getTenants } from '../services/dataService';
+import { User, Tenant } from '../types';
export const TeamManagement: React.FC = () => {
const [users, setUsers] = useState([]);
const [teams, setTeams] = useState([]);
+ const [tenants, setTenants] = useState([]);
const [loading, setLoading] = useState(true);
const [isSaving, setIsSaving] = useState(false);
const [isModalOpen, setIsModalOpen] = useState(false);
@@ -20,7 +21,8 @@ export const TeamManagement: React.FC = () => {
email: '',
role: 'agent' as any,
team_id: '',
- status: 'active' as any
+ status: 'active' as any,
+ tenant_id: ''
});
const loadData = async () => {
@@ -29,9 +31,28 @@ export const TeamManagement: React.FC = () => {
if (!tid) return;
setLoading(true);
try {
- const [fu, ft, me] = await Promise.all([getUsers(tid), getTeams(tid), uid ? getUserById(uid) : null]);
- setUsers(fu.filter(u => u.role !== 'super_admin'));
- setTeams(ft);
+ const me = uid ? await getUserById(uid) : null;
+
+ const isSuperAdmin = me?.role === 'super_admin';
+ const effectiveTid = isSuperAdmin ? 'all' : tid;
+
+ const promises: Promise[] = [
+ getUsers(effectiveTid),
+ getTeams(effectiveTid)
+ ];
+
+ if (isSuperAdmin) {
+ promises.push(getTenants());
+ }
+
+ const results = await Promise.all(promises);
+
+ setUsers(isSuperAdmin ? results[0] : results[0].filter((u: User) => u.role !== 'super_admin'));
+ setTeams(results[1]);
+ if (isSuperAdmin && results[2]) {
+ setTenants(results[2]);
+ }
+
if (me) setCurrentUser(me);
} catch (err) { console.error(err); } finally { setLoading(false); }
};
@@ -43,11 +64,13 @@ export const TeamManagement: React.FC = () => {
setIsSaving(true);
try {
const tid = localStorage.getItem('ctms_tenant_id') || '';
+ const finalTenantId = currentUser?.role === 'super_admin' ? formData.tenant_id : tid;
+
if (editingUser) {
- const success = await updateUser(editingUser.id, formData);
+ const success = await updateUser(editingUser.id, { ...formData, tenant_id: finalTenantId });
if (success) { setIsModalOpen(false); loadData(); }
} else {
- await createMember({ ...formData, tenant_id: tid });
+ await createMember({ ...formData, tenant_id: finalTenantId });
setIsModalOpen(false);
loadData();
}
@@ -72,6 +95,7 @@ export const TeamManagement: React.FC = () => {
const filtered = users.filter(u => u.name.toLowerCase().includes(searchTerm.toLowerCase()) || u.email.toLowerCase().includes(searchTerm.toLowerCase()));
const getRoleLabel = (role: string) => {
+ if (role === 'super_admin') return 'Super Admin';
if (role === 'admin') return 'Admin';
if (role === 'manager') return 'Gerente';
return 'Agente';
@@ -111,6 +135,7 @@ export const TeamManagement: React.FC = () => {
| UsuƔrio |
+ {currentUser?.role === 'super_admin' && Organização | }
Função |
Time |
Status |
@@ -138,6 +163,13 @@ export const TeamManagement: React.FC = () => {
+ {currentUser?.role === 'super_admin' && (
+
+
+ {tenants.find(t => t.id === user.tenant_id)?.name || user.tenant_id}
+
+ |
+ )}
{getRoleLabel(user.role)}
|
@@ -148,7 +180,7 @@ export const TeamManagement: React.FC = () => {
{canManage && (
-
+
|
@@ -173,6 +205,15 @@ export const TeamManagement: React.FC = () => {
setFormData({...formData, email:e.target.value})} className="w-full bg-white dark:bg-dark-input border border-zinc-200 dark:border-dark-border p-3 rounded-lg text-sm text-zinc-900 dark:text-dark-text disabled:bg-zinc-50 dark:disabled:bg-dark-bg/50 dark:disabled:text-dark-muted" disabled={!!editingUser} required />
+ {currentUser?.role === 'super_admin' && (
+
+
+
+
+ )}
@@ -186,7 +227,7 @@ export const TeamManagement: React.FC = () => {
diff --git a/services/dataService.ts b/services/dataService.ts
index 8407d22..0c1a9cf 100644
--- a/services/dataService.ts
+++ b/services/dataService.ts
@@ -248,6 +248,33 @@ export const createTenant = async (tenantData: any): Promise => {
}
};
+export const updateTenant = async (id: string, tenantData: any): Promise => {
+ try {
+ const response = await fetch(`${API_URL}/tenants/${id}`, {
+ method: 'PUT',
+ headers: getHeaders(),
+ body: JSON.stringify(tenantData)
+ });
+ return response.ok;
+ } catch (error) {
+ console.error("API Error (updateTenant):", error);
+ return false;
+ }
+};
+
+export const deleteTenant = async (id: string): Promise => {
+ try {
+ const response = await fetch(`${API_URL}/tenants/${id}`, {
+ method: 'DELETE',
+ headers: getHeaders()
+ });
+ return response.ok;
+ } catch (error) {
+ console.error("API Error (deleteTenant):", error);
+ return false;
+ }
+};
+
// --- Auth Functions ---
export const logout = () => {