diff --git a/backend/index.js b/backend/index.js index e24e156..ef69434 100644 --- a/backend/index.js +++ b/backend/index.js @@ -286,7 +286,17 @@ apiRouter.get('/users', async (req, res) => { let q = 'SELECT * FROM users'; const params = []; - if (effectiveTenantId && effectiveTenantId !== 'all') { q += ' WHERE tenant_id = ?'; params.push(effectiveTenantId); } + if (effectiveTenantId && effectiveTenantId !== 'all') { + q += ' WHERE tenant_id = ?'; + params.push(effectiveTenantId); + } + + // Strict RBAC: Managers can only see users in their own team + if (req.user.role === 'manager') { + q += (params.length > 0 ? ' AND' : ' WHERE') + ' team_id = ?'; + params.push(req.user.team_id); + } + const [rows] = await pool.query(q, params); res.json(rows); } catch (error) { res.status(500).json({ error: error.message }); } @@ -447,17 +457,27 @@ apiRouter.get('/attendances', async (req, res) => { if (req.user.role === 'agent') { q += ' AND a.user_id = ?'; params.push(req.user.id); - } else if (userId && userId !== 'all') { - // check if it's a slug or id - if (userId.startsWith('u_')) { - q += ' AND a.user_id = ?'; - params.push(userId); - } else { - q += ' AND u.slug = ?'; - params.push(userId); + } else { + if (req.user.role === 'manager') { + q += ' AND u.team_id = ?'; + params.push(req.user.team_id); + } else if (teamId && teamId !== 'all') { + q += ' AND u.team_id = ?'; + params.push(teamId); + } + + if (userId && userId !== 'all') { + // check if it's a slug or id + if (userId.startsWith('u_')) { + q += ' AND a.user_id = ?'; + params.push(userId); + } else { + q += ' AND u.slug = ?'; + params.push(userId); + } } } - if (teamId && teamId !== 'all') { q += ' AND u.team_id = ?'; params.push(teamId); } + if (funnelStage && funnelStage !== 'all') { q += ' AND a.funnel_stage = ?'; params.push(funnelStage); } if (origin && origin !== 'all') { q += ' AND a.origin = ?'; params.push(origin); } @@ -513,6 +533,13 @@ apiRouter.get('/teams', async (req, res) => { q += ' WHERE tenant_id = ?'; params.push(effectiveTenantId); } + + // Strict RBAC: Managers can only see their own team + if (req.user.role === 'manager') { + q += (params.length > 0 ? ' AND' : ' WHERE') + ' id = ?'; + params.push(req.user.team_id); + } + const [rows] = await pool.query(q, params); res.json(rows); } catch (error) { @@ -520,7 +547,7 @@ apiRouter.get('/teams', async (req, res) => { } }); -apiRouter.post('/teams', requireRole(['admin', 'manager', 'owner', 'super_admin']), async (req, res) => { +apiRouter.post('/teams', requireRole(['admin', 'owner', 'super_admin']), async (req, res) => { const { name, description, tenantId } = req.body; const effectiveTenantId = req.user.role === 'super_admin' ? tenantId : req.user.tenant_id; try { @@ -536,7 +563,7 @@ apiRouter.post('/teams', requireRole(['admin', 'manager', 'owner', 'super_admin' } }); -apiRouter.put('/teams/:id', requireRole(['admin', 'manager', 'owner', 'super_admin']), async (req, res) => { +apiRouter.put('/teams/:id', requireRole(['admin', 'owner', 'super_admin']), async (req, res) => { const { name, description } = req.body; try { const [existing] = await pool.query('SELECT tenant_id FROM teams WHERE id = ?', [req.params.id]); diff --git a/pages/Dashboard.tsx b/pages/Dashboard.tsx index 3218f86..b7e597d 100644 --- a/pages/Dashboard.tsx +++ b/pages/Dashboard.tsx @@ -253,14 +253,16 @@ export const Dashboard: React.FC = () => { {users.map(u => {u.name})} - handleFilterChange('teamId', e.target.value)} - > - Todas Equipes - {teams.map(t => {t.name})} - + {currentUser?.role !== 'manager' && ( + handleFilterChange('teamId', e.target.value)} + > + Todas Equipes + {teams.map(t => {t.name})} + + )} > )} diff --git a/pages/Teams.tsx b/pages/Teams.tsx index ab4a3d6..b72b85c 100644 --- a/pages/Teams.tsx +++ b/pages/Teams.tsx @@ -1,12 +1,13 @@ import React, { useState, useEffect, useMemo } from 'react'; import { Building2, Users, Plus, Search, Target, ArrowUpRight, Loader2, Edit2, X } from 'lucide-react'; -import { getTeams, getUsers, getAttendances, createTeam, updateTeam } from '../services/dataService'; +import { getTeams, getUsers, getAttendances, createTeam, updateTeam, getUserById } from '../services/dataService'; import { User, Attendance } from '../types'; export const Teams: React.FC = () => { const [teams, setTeams] = useState([]); const [users, setUsers] = useState([]); const [attendances, setAttendances] = useState([]); + const [currentUser, setCurrentUser] = useState(null); const [loading, setLoading] = useState(true); const [isSaving, setIsSaving] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false); @@ -15,15 +16,18 @@ export const Teams: React.FC = () => { const loadData = async () => { const tid = localStorage.getItem('ctms_tenant_id'); + const uid = localStorage.getItem('ctms_user_id'); if (!tid) return; setLoading(true); try { - const [ft, fu, fa] = await Promise.all([ + const [ft, fu, fa, me] = await Promise.all([ getTeams(tid), getUsers(tid), - getAttendances(tid, { dateRange: { start: new Date(0), end: new Date() }, userId: 'all', teamId: 'all' }) + getAttendances(tid, { dateRange: { start: new Date(0), end: new Date() }, userId: 'all', teamId: 'all' }), + uid ? getUserById(uid) : null ]); setTeams(ft); setUsers(fu); setAttendances(fa); + if (me) setCurrentUser(me); } catch (e) { console.error(e); } finally { setLoading(false); } }; @@ -51,6 +55,8 @@ export const Teams: React.FC = () => { if (loading && teams.length === 0) return Carregando...; + const canManage = currentUser?.role === 'admin' || currentUser?.role === 'super_admin'; + return ( @@ -58,9 +64,11 @@ export const Teams: React.FC = () => { Times Visualize o desempenho e gerencie seus grupos de vendas. - { setEditingTeam(null); setFormData({name:'', description:''}); setIsModalOpen(true); }} className="bg-zinc-900 dark:bg-brand-yellow text-white dark:text-zinc-950 px-4 py-2 rounded-lg flex items-center gap-2 text-sm font-bold shadow-sm hover:opacity-90 transition-all"> - Novo Time - + {canManage && ( + { setEditingTeam(null); setFormData({name:'', description:''}); setIsModalOpen(true); }} className="bg-zinc-900 dark:bg-brand-yellow text-white dark:text-zinc-950 px-4 py-2 rounded-lg flex items-center gap-2 text-sm font-bold shadow-sm hover:opacity-90 transition-all"> + Novo Time + + )} @@ -68,7 +76,9 @@ export const Teams: React.FC = () => { - { setEditingTeam(t); setFormData({name:t.name, description:t.description||''}); setIsModalOpen(true); }} className="text-zinc-400 dark:text-dark-muted hover:text-zinc-900 dark:hover:text-dark-text opacity-0 group-hover:opacity-100 p-2 rounded-lg hover:bg-zinc-50 dark:hover:bg-dark-bg transition-all"> + {canManage && ( + { setEditingTeam(t); setFormData({name:t.name, description:t.description||''}); setIsModalOpen(true); }} className="text-zinc-400 dark:text-dark-muted hover:text-zinc-900 dark:hover:text-dark-text opacity-0 group-hover:opacity-100 p-2 rounded-lg hover:bg-zinc-50 dark:hover:bg-dark-bg transition-all"> + )} {t.name} {t.description || 'Sem descrição definida.'}
Visualize o desempenho e gerencie seus grupos de vendas.
{t.description || 'Sem descrição definida.'}