diff --git a/agenciac_comia.sql b/agenciac_comia.sql index 1afa28f..7028445 100644 --- a/agenciac_comia.sql +++ b/agenciac_comia.sql @@ -147,6 +147,7 @@ CREATE TABLE `users` ( `name` varchar(255) NOT NULL, `email` varchar(255) NOT NULL, `password_hash` varchar(255) NOT NULL DEFAULT 'hash_placeholder', + `slug` varchar(255) UNIQUE DEFAULT NULL, `avatar_url` text, `role` enum('super_admin','admin','manager','agent') NOT NULL DEFAULT 'agent', `bio` text, diff --git a/backend/index.js b/backend/index.js index 1e5932f..4de1c3d 100644 --- a/backend/index.js +++ b/backend/index.js @@ -203,7 +203,7 @@ apiRouter.post('/auth/forgot-password', async (req, res) => { const [users] = await pool.query('SELECT name FROM users WHERE email = ?', [email]); if (users.length > 0) { const token = crypto.randomBytes(32).toString('hex'); - await pool.query('INSERT INTO password_resets (email, token, expires_at) VALUES (?, ?, DATE_ADD(NOW(), INTERVAL 1 HOUR))', [email, token]); + await pool.query('INSERT INTO password_resets (email, token, expires_at) VALUES (?, ?, DATE_ADD(NOW(), INTERVAL 15 MINUTE))', [email, token]); const link = `${process.env.APP_URL || 'http://localhost:3001'}/#/reset-password?token=${token}`; const mailOptions = { @@ -218,7 +218,7 @@ apiRouter.post('/auth/forgot-password', async (req, res) => {
Redefinir Minha Senha
-

Este link expira em 1 hora. Se você não solicitou isso, pode ignorar este e-mail.

+

Este link expira em 15 minutos. Se você não solicitou isso, pode ignorar este e-mail.

Desenvolvido por Blyzer

@@ -236,14 +236,23 @@ apiRouter.post('/auth/forgot-password', async (req, res) => { // Reset Password apiRouter.post('/auth/reset-password', async (req, res) => { - const { token, password } = req.body; + const { token, password, name } = req.body; try { const [resets] = await pool.query('SELECT email FROM password_resets WHERE token = ? AND expires_at > NOW()', [token]); - if (resets.length === 0) return res.status(400).json({ error: 'Token inválido.' }); + if (resets.length === 0) return res.status(400).json({ error: 'Token inválido ou expirado.' }); + const hash = await bcrypt.hash(password, 10); - await pool.query('UPDATE users SET password_hash = ? WHERE email = ?', [hash, resets[0].email]); + + if (name) { + // If a name is provided (like in the initial admin setup flow), update it along with the password + await pool.query('UPDATE users SET password_hash = ?, name = ? WHERE email = ?', [hash, name, resets[0].email]); + } else { + // Standard password reset, just update the hash + await pool.query('UPDATE users SET password_hash = ? WHERE email = ?', [hash, resets[0].email]); + } + await pool.query('DELETE FROM password_resets WHERE email = ?', [resets[0].email]); - res.json({ message: 'Senha resetada.' }); + res.json({ message: 'Senha e perfil atualizados com sucesso.' }); } catch (error) { res.status(500).json({ error: error.message }); } @@ -263,9 +272,9 @@ apiRouter.get('/users', async (req, res) => { } catch (error) { res.status(500).json({ error: error.message }); } }); -apiRouter.get('/users/:id', async (req, res) => { +apiRouter.get('/users/:idOrSlug', async (req, res) => { try { - const [rows] = await pool.query('SELECT * FROM users WHERE id = ?', [req.params.id]); + const [rows] = await pool.query('SELECT * FROM users WHERE id = ? OR slug = ?', [req.params.idOrSlug, req.params.idOrSlug]); if (rows.length === 0) return res.status(404).json({ error: 'Not found' }); if (req.user.role !== 'super_admin' && rows[0].tenant_id !== req.user.tenant_id) { return res.status(403).json({ error: 'Acesso negado.' }); @@ -295,33 +304,32 @@ apiRouter.post('/users', requireRole(['admin', 'owner', 'super_admin']), async ( // 3. Gerar Token de Setup de Senha (reusando lógica de reset) 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))', + 'INSERT INTO password_resets (email, token, expires_at) VALUES (?, ?, DATE_ADD(NOW(), INTERVAL 15 MINUTE))', [email, token] ); // 4. Enviar E-mail de Boas-vindas 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: email, - subject: 'Bem-vindo ao Fasto - Crie sua senha', + subject: 'Bem-vindo ao Fasto - Finalize seu cadastro', html: ` -
+

Olá, ${name}!

Você foi convidado para participar da equipe no Fasto.

Clique no botão abaixo para definir sua senha e acessar sua conta:

- Definir Minha Senha + Finalizar Cadastro
-

Este link expira em 24 horas. Se você não esperava este convite, ignore este e-mail.

+

Este link expira em 15 minutos. Se você não esperava este convite, ignore este e-mail.

Desenvolvido por Blyzer

` }); - res.status(201).json({ id: uid, message: 'Convite enviado com sucesso.' }); } catch (error) { console.error('Invite error:', error); @@ -406,8 +414,23 @@ apiRouter.get('/attendances', async (req, res) => { let q = 'SELECT a.*, u.team_id FROM attendances a JOIN users u ON a.user_id = u.id WHERE a.tenant_id = ?'; const params = [effectiveTenantId]; + if (startDate && endDate) { q += ' AND a.created_at BETWEEN ? AND ?'; params.push(new Date(startDate), new Date(endDate)); } - if (userId && userId !== 'all') { q += ' AND a.user_id = ?'; params.push(userId); } + + // Strict RBAC: Agents can ONLY see their own data, regardless of what they request + 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); + } + } 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); } @@ -525,19 +548,32 @@ apiRouter.post('/tenants', requireRole(['super_admin']), async (req, res) => { 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]); + // Adicionar aos pending_registrations em vez de criar um usuário direto se quisermos que eles completem o cadastro. + // Ou, se quisermos apenas definir a senha, mantemos o password_resets. + // O usuário quer "like a register", então vamos enviar para uma página onde eles definem a senha. + await connection.query('INSERT INTO password_resets (email, token, expires_at) VALUES (?, ?, DATE_ADD(NOW(), INTERVAL 15 MINUTE))', [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.

` + subject: 'Bem-vindo ao Fasto - Conclua seu cadastro de Admin', + html: ` +
+

Sua organização foi criada

+

Você foi definido como administrador da organização ${name}.

+

Por favor, clique no botão abaixo para definir sua senha e concluir seu cadastro.

+
+ Finalizar Cadastro +
+

Este link expira em 15 minutos.

+
+ ` }).catch(err => console.error("Email failed:", err)); } await connection.commit(); - res.status(201).json({ id: tid }); + res.status(201).json({ id: tid, message: 'Organização criada e convite enviado por e-mail.' }); } catch (error) { await connection.rollback(); res.status(500).json({ error: error.message }); } finally { connection.release(); } }); @@ -592,7 +628,7 @@ const provisionSuperAdmin = async () => { 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))', + 'INSERT INTO password_resets (email, token, expires_at) VALUES (?, ?, DATE_ADD(NOW(), INTERVAL 15 MINUTE))', [email, token] ); @@ -604,13 +640,13 @@ const provisionSuperAdmin = async () => { to: email, subject: 'Conta Super Admin Criada - Fasto', html: ` -
-

Conta Super Admin Gerada

-

Sua conta de suporte (super_admin) foi criada no Fasto.

+
+

Conta Super Admin Gerada

+

Sua conta de suporte (super_admin) foi criada no Fasto.

- Definir Senha do Super Admin + Finalizar Cadastro
-

Este link expira em 24 horas.

+

Este link expira em 15 minutos.

` }).catch(err => console.error("Failed to send super_admin email:", err)); diff --git a/components/KPICard.tsx b/components/KPICard.tsx index 4019149..0a6cf46 100644 --- a/components/KPICard.tsx +++ b/components/KPICard.tsx @@ -12,9 +12,10 @@ interface KPICardProps { } export const KPICard: React.FC = ({ title, value, subValue, trend, trendValue, icon: Icon, colorClass = "text-blue-600" }) => { - // Extract base color from colorClass (e.g., 'text-yellow-600' -> 'yellow') - const baseColor = colorClass.split('-')[1] || 'blue'; - + // Extract base color from colorClass (e.g., 'text-brand-yellow' -> 'brand-yellow' or 'text-blue-600' -> 'blue-600') + // Safer extraction: + const baseColor = colorClass.replace('text-', '').split(' ')[0]; // gets 'brand-yellow' or 'zinc-500' + return (
@@ -22,8 +23,8 @@ export const KPICard: React.FC = ({ title, value, subValue, trend,

{title}

{value}
-
- +
+
@@ -31,6 +32,7 @@ export const KPICard: React.FC = ({ title, value, subValue, trend,
{trend === 'up' && ▲ {trendValue}} {trend === 'down' && ▼ {trendValue}} + {trend === 'neutral' && - {trendValue}} {subValue && {subValue}}
)} diff --git a/components/SellersTable.tsx b/components/SellersTable.tsx index 7509db7..48a1684 100644 --- a/components/SellersTable.tsx +++ b/components/SellersTable.tsx @@ -106,7 +106,7 @@ export const SellersTable: React.FC = ({ data }) => { navigate(`/users/${item.user.id}`)} + onClick={() => navigate(`/users/${item.user.slug || item.user.id}`)} >
diff --git a/docker-compose.local.yml b/docker-compose.local.yml index b25d315..3c2e2a4 100644 --- a/docker-compose.local.yml +++ b/docker-compose.local.yml @@ -2,6 +2,7 @@ services: app: build: . container_name: fasto-app-local + restart: unless-stopped ports: - "3001:3001" environment: @@ -27,6 +28,7 @@ services: db: image: mysql:8.0 container_name: fasto-db-local + restart: unless-stopped environment: MYSQL_ROOT_PASSWORD: ${DB_PASSWORD:-root_password} MYSQL_DATABASE: ${DB_NAME:-agenciac_comia} diff --git a/pages/Dashboard.tsx b/pages/Dashboard.tsx index e3e019d..3218f86 100644 --- a/pages/Dashboard.tsx +++ b/pages/Dashboard.tsx @@ -25,6 +25,7 @@ interface SellerStats { export const Dashboard: React.FC = () => { const [loading, setLoading] = useState(true); const [data, setData] = useState([]); + const [prevData, setPrevData] = useState([]); const [users, setUsers] = useState([]); const [teams, setTeams] = useState([]); const [currentUser, setCurrentUser] = useState(null); @@ -48,16 +49,24 @@ export const Dashboard: React.FC = () => { const storedUserId = localStorage.getItem('ctms_user_id'); if (!tenantId) return; + // Calculate previous date range for accurate period-over-period trend + const duration = filters.dateRange.end.getTime() - filters.dateRange.start.getTime(); + const prevStart = new Date(filters.dateRange.start.getTime() - duration); + const prevEnd = new Date(filters.dateRange.start.getTime()); + const prevFilters = { ...filters, dateRange: { start: prevStart, end: prevEnd } }; + // Fetch users, attendances, teams and current user in parallel - const [fetchedUsers, fetchedData, fetchedTeams, me] = await Promise.all([ + const [fetchedUsers, fetchedData, prevFetchedData, fetchedTeams, me] = await Promise.all([ getUsers(tenantId), getAttendances(tenantId, filters), + getAttendances(tenantId, prevFilters), getTeams(tenantId), storedUserId ? getUserById(storedUserId) : null ]); setUsers(fetchedUsers); setData(fetchedData); + setPrevData(prevFetchedData); setTeams(fetchedTeams); if (me) setCurrentUser(me); } catch (error) { @@ -74,6 +83,48 @@ export const Dashboard: React.FC = () => { const avgScore = data.length > 0 ? (data.reduce((acc, curr) => acc + curr.score, 0) / data.length).toFixed(1) : "0"; const avgResponseTime = data.length > 0 ? (data.reduce((acc, curr) => acc + curr.first_response_time_min, 0) / data.length).toFixed(0) : "0"; const avgHandleTime = data.length > 0 ? (data.reduce((acc, curr) => acc + curr.handling_time_min, 0) / data.length).toFixed(0) : "0"; + + // --- Dynamic Trends Calculation --- + const trends = useMemo(() => { + if (data.length === 0 && prevData.length === 0) { + return { leads: null, score: null, resp: null }; + } + + const calcAvg = (arr: Attendance[], key: keyof Attendance) => + arr.length ? arr.reduce((acc, curr) => acc + (curr[key] as number), 0) / arr.length : 0; + + // Leads Trend (%) + const recentLeads = data.length; + const prevLeads = prevData.length; + let leadsTrend = 0; + if (prevLeads > 0) { + leadsTrend = ((recentLeads - prevLeads) / prevLeads) * 100; + } else if (recentLeads > 0) { + leadsTrend = 100; + } + + // Score Trend (Absolute point difference) + const recentScore = calcAvg(data, 'score'); + const prevScore = calcAvg(prevData, 'score'); + let scoreTrend = 0; + if (prevData.length > 0) { + scoreTrend = recentScore - prevScore; + } else { + scoreTrend = recentScore; + } + + // Response Time Trend (%) + const recentResp = calcAvg(data, 'first_response_time_min'); + const prevResp = calcAvg(prevData, 'first_response_time_min'); + let respTrend = 0; + if (prevResp > 0) { + respTrend = ((recentResp - prevResp) / prevResp) * 100; + } else if (recentResp > 0) { + respTrend = 100; // Time increased from 0, which is worse + } + + return { leads: leadsTrend, score: scoreTrend, resp: respTrend }; + }, [data, prevData]); // --- Chart Data: Funnel --- const funnelData = useMemo(() => { @@ -245,25 +296,25 @@ export const Dashboard: React.FC = () => { 0 ? 'up' : trends.leads < 0 ? 'down' : 'neutral'} + trendValue={`${Math.abs(trends.leads).toFixed(1)}%`} icon={Users} - colorClass="text-yellow-600" + colorClass="text-brand-yellow" /> 75 ? 'up' : 'down'} - trendValue="2.1" + trend={trends.score > 0 ? 'up' : trends.score < 0 ? 'down' : 'neutral'} + trendValue={Math.abs(trends.score).toFixed(1)} icon={TrendingUp} - colorClass="text-zinc-600" + colorClass="text-zinc-500" /> 0 ? 'down' : 'neutral'} // Faster response is better (up) + trendValue={`${Math.abs(trends.resp).toFixed(1)}%`} icon={Clock} colorClass="text-orange-500" /> diff --git a/pages/Login.tsx b/pages/Login.tsx index 3a006ec..c9ea423 100644 --- a/pages/Login.tsx +++ b/pages/Login.tsx @@ -58,12 +58,6 @@ export const Login: React.FC = () => {

Acesse sua conta

-

- Ou{' '} - - registre sua nova organização - -

diff --git a/pages/ResetPassword.tsx b/pages/ResetPassword.tsx index 70094fd..31a90ab 100644 --- a/pages/ResetPassword.tsx +++ b/pages/ResetPassword.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState } from 'react'; import { useNavigate, useLocation, Link } from 'react-router-dom'; import { Hexagon, Lock, ArrowRight, Loader2, CheckCircle2, AlertCircle } from 'lucide-react'; import { resetPassword } from '../services/dataService'; @@ -26,7 +26,7 @@ export const ResetPassword: React.FC = () => { setError(''); try { - await resetPassword(password, token); + await resetPassword(password, token); // No name sent here setIsSuccess(true); setTimeout(() => navigate('/login'), 3000); } catch (err: any) { diff --git a/pages/SetupAccount.tsx b/pages/SetupAccount.tsx new file mode 100644 index 0000000..07d0fd9 --- /dev/null +++ b/pages/SetupAccount.tsx @@ -0,0 +1,156 @@ +import React, { useState, useEffect } from 'react'; +import { useNavigate, useLocation, Link } from 'react-router-dom'; +import { Hexagon, Lock, ArrowRight, Loader2, CheckCircle2, AlertCircle, User } from 'lucide-react'; +import { resetPassword } from '../services/dataService'; + +export const ResetPassword: React.FC = () => { + const navigate = useNavigate(); + const location = useLocation(); + const query = new URLSearchParams(location.search); + const token = query.get('token') || ''; + + const [name, setName] = useState(''); + const [password, setPassword] = useState(''); + const [confirmPassword, setConfirmPassword] = useState(''); + const [isLoading, setIsLoading] = useState(false); + const [isSuccess, setIsSuccess] = useState(false); + const [error, setError] = useState(''); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (password !== confirmPassword) { + setError('As senhas não coincidem.'); + return; + } + + setIsLoading(true); + setError(''); + + try { + await resetPassword(password, token, name); + setIsSuccess(true); + setTimeout(() => navigate('/login'), 3000); + } catch (err: any) { + setError(err.message || 'Erro ao redefinir senha. O link pode estar expirado.'); + } finally { + setIsLoading(false); + } + }; + + return ( +
+
+
+
+ +
+ Fasto. +
+

+ Finalize seu cadastro +

+
+ +
+
+ {isSuccess ? ( +
+
+ +
+

Tudo pronto!

+

+ Seu perfil foi atualizado com sucesso. Redirecionando para o login... +

+
+ ) : ( +
+ {error && ( +
+ + {error} +
+ )} + +
+ +
+
+ +
+ setName(e.target.value)} + className="block w-full pl-10 pr-3 py-2 border border-zinc-300 dark:border-dark-border rounded-lg bg-white dark:bg-dark-input text-zinc-900 dark:text-zinc-100 placeholder-zinc-400 dark:placeholder-zinc-600 focus:ring-brand-yellow/20 focus:border-brand-yellow sm:text-sm transition-all" + placeholder="João da Silva" + /> +
+
+ +
+ +
+
+ +
+ setPassword(e.target.value)} + className="block w-full pl-10 pr-3 py-2 border border-zinc-300 dark:border-dark-border rounded-lg bg-white dark:bg-dark-input text-zinc-900 dark:text-zinc-100 placeholder-zinc-400 dark:placeholder-zinc-600 focus:ring-brand-yellow/20 focus:border-brand-yellow sm:text-sm transition-all" + placeholder="••••••••" + /> +
+
+ +
+ +
+
+ +
+ setConfirmPassword(e.target.value)} + className="block w-full pl-10 pr-3 py-2 border border-zinc-300 dark:border-dark-border rounded-lg bg-white dark:bg-dark-input text-zinc-900 dark:text-zinc-100 placeholder-zinc-400 dark:placeholder-zinc-600 focus:ring-brand-yellow/20 focus:border-brand-yellow sm:text-sm transition-all" + placeholder="••••••••" + /> +
+
+ +
+ +
+
+ )} +
+
+
+ ); +}; diff --git a/pages/SuperAdmin.tsx b/pages/SuperAdmin.tsx index 0cd5c34..7ef59eb 100644 --- a/pages/SuperAdmin.tsx +++ b/pages/SuperAdmin.tsx @@ -1,9 +1,9 @@ import React, { useState, useMemo } from 'react'; import { Building2, Users, MessageSquare, Plus, Search, - Edit, Trash2, ChevronDown, ChevronUp, ChevronsUpDown, X + Edit, Trash2, ChevronDown, ChevronUp, ChevronsUpDown, X, CheckCircle2 } from 'lucide-react'; -import { getTenants, createTenant, deleteTenant } from '../services/dataService'; +import { getTenants, createTenant, deleteTenant, updateTenant } from '../services/dataService'; import { Tenant } from '../types'; import { DateRangePicker } from '../components/DateRangePicker'; import { KPICard } from '../components/KPICard'; @@ -91,21 +91,44 @@ export const SuperAdmin: React.FC = () => { } }; + const [successMessage, setSuccessMessage] = useState(''); + const [errorMessage, setErrorMessage] = useState(''); + const handleSaveTenant = async (e: React.FormEvent) => { e.preventDefault(); + setErrorMessage(''); + setSuccessMessage(''); const form = e.target as HTMLFormElement; 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(); - alert('Organização salva com sucesso!'); + + if (editingTenant) { + const success = await updateTenant(editingTenant.id, { name, slug, admin_email, status }); + if (success) { + setSuccessMessage('Organização atualizada com sucesso!'); + loadTenants(); + setTimeout(() => { + setIsModalOpen(false); + setSuccessMessage(''); + setEditingTenant(null); + }, 2000); + } else { + setErrorMessage('Erro ao atualizar organização.'); + } } else { - alert('Erro ao salvar organização.'); + const result = await createTenant({ name, slug, admin_email, status }); + if (result.success) { + setSuccessMessage(result.message || 'Organização criada com sucesso!'); + loadTenants(); + setTimeout(() => { + setIsModalOpen(false); + setSuccessMessage(''); + }, 3000); + } else { + setErrorMessage(result.message || 'Erro ao salvar organização.'); + } } }; @@ -132,7 +155,7 @@ export const SuperAdmin: React.FC = () => {

Painel Super Admin

Gerencie organizações e visualize estatísticas globais.

-
@@ -225,6 +248,16 @@ export const SuperAdmin: React.FC = () => {
+ {successMessage && ( +
+ {successMessage} +
+ )} + {errorMessage && ( +
+ {errorMessage} +
+ )}
diff --git a/pages/TeamManagement.tsx b/pages/TeamManagement.tsx index 1689302..e5ebe47 100644 --- a/pages/TeamManagement.tsx +++ b/pages/TeamManagement.tsx @@ -1,4 +1,5 @@ import React, { useState, useEffect } from 'react'; +import { Link } from 'react-router-dom'; import { Users, Plus, Mail, Search, X, Edit, Trash2, Loader2, AlertTriangle } from 'lucide-react'; import { getUsers, getTeams, createMember, deleteUser, updateUser, getUserById, getTenants } from '../services/dataService'; import { User, Tenant } from '../types'; @@ -12,6 +13,7 @@ export const TeamManagement: React.FC = () => { const [isModalOpen, setIsModalOpen] = useState(false); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const [searchTerm, setSearchTerm] = useState(''); + const [tenantFilter, setTenantFilter] = useState('all'); const [currentUser, setCurrentUser] = useState(null); const [userToDelete, setUserToDelete] = useState(null); const [deleteConfirmName, setDeleteConfirmName] = useState(''); @@ -92,7 +94,10 @@ export const TeamManagement: React.FC = () => { if (loading && users.length === 0) return
Carregando...
; const canManage = currentUser?.role === 'admin' || currentUser?.role === 'super_admin'; - const filtered = users.filter(u => u.name.toLowerCase().includes(searchTerm.toLowerCase()) || u.email.toLowerCase().includes(searchTerm.toLowerCase())); + const filtered = users.filter(u => + (u.name.toLowerCase().includes(searchTerm.toLowerCase()) || u.email.toLowerCase().includes(searchTerm.toLowerCase())) && + (tenantFilter === 'all' || u.tenant_id === tenantFilter) + ); const getRoleLabel = (role: string) => { if (role === 'super_admin') return 'Super Admin'; @@ -124,11 +129,21 @@ export const TeamManagement: React.FC = () => {
-
-
+
+
setSearchTerm(e.target.value)} className="w-full pl-9 pr-4 py-2 bg-white dark:bg-dark-bg border border-zinc-200 dark:border-dark-border rounded-lg text-sm text-zinc-900 dark:text-dark-text outline-none focus:ring-2 focus:ring-brand-yellow/20 transition-all" />
+ {currentUser?.role === 'super_admin' && ( + + )}
@@ -158,7 +173,7 @@ export const TeamManagement: React.FC = () => {
-
{user.name}
+ {user.name}
{user.email}
diff --git a/pages/UserDetail.tsx b/pages/UserDetail.tsx index f745210..78a1ebb 100644 --- a/pages/UserDetail.tsx +++ b/pages/UserDetail.tsx @@ -3,6 +3,7 @@ import { useParams, Link } from 'react-router-dom'; import { getAttendances, getUserById } from '../services/dataService'; import { Attendance, User, FunnelStage, DashboardFilter } from '../types'; import { ArrowLeft, Mail, Phone, Clock, MessageSquare, ChevronLeft, ChevronRight, Eye, Filter } from 'lucide-react'; +import { DateRangePicker } from '../components/DateRangePicker'; const ITEMS_PER_PAGE = 10; @@ -13,7 +14,7 @@ export const UserDetail: React.FC = () => { const [loading, setLoading] = useState(true); const [currentPage, setCurrentPage] = useState(1); const [filters, setFilters] = useState({ - dateRange: { start: new Date(0), end: new Date() }, + dateRange: { start: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000), end: new Date() }, userId: id, funnelStage: 'all', origin: 'all' @@ -121,6 +122,11 @@ export const UserDetail: React.FC = () => { Filtros: + handleFilterChange('dateRange', range)} + /> +