From 871d1eb8508e9da47cee3026fea05178cbc6dfe9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Faleiros?= Date: Fri, 29 May 2026 14:42:07 -0300 Subject: [PATCH] Extract remaining backend routes --- backend/index.js | 608 +-------------------------- backend/routes/apiKeysRoutes.js | 61 +++ backend/routes/attendancesRoutes.js | 88 ++++ backend/routes/funnelsRoutes.js | 141 +++++++ backend/routes/integrationsRoutes.js | 155 +++++++ backend/routes/originsRoutes.js | 138 ++++++ backend/routes/searchRoutes.js | 80 ++++ 7 files changed, 676 insertions(+), 595 deletions(-) create mode 100644 backend/routes/apiKeysRoutes.js create mode 100644 backend/routes/attendancesRoutes.js create mode 100644 backend/routes/funnelsRoutes.js create mode 100644 backend/routes/integrationsRoutes.js create mode 100644 backend/routes/originsRoutes.js create mode 100644 backend/routes/searchRoutes.js diff --git a/backend/index.js b/backend/index.js index 21169d4..3e05a74 100644 --- a/backend/index.js +++ b/backend/index.js @@ -6,18 +6,21 @@ const multer = require('multer'); const { v4: uuidv4 } = require('uuid'); const fs = require('fs'); const pool = require('./db'); -const { hashSecret, maskSecret } = require('./utils/security'); const transporter = require('./services/mailer'); const { createCorsMiddleware } = require('./config/cors'); const { allowedOrigins, getBaseUrl, getStartupBaseUrl, isProduction, jwtSecret: JWT_SECRET, port: PORT } = require('./config/runtime'); -const { authenticateToken, requireRole } = require('./middleware/auth'); +const { authenticateToken } = require('./middleware/auth'); const { createAuthRouter } = require('./routes/authRoutes'); const { createUsersRouter } = require('./routes/usersRoutes'); const { createTeamsRouter } = require('./routes/teamsRoutes'); const { createTenantsRouter } = require('./routes/tenantsRoutes'); const { createActivityRouter } = require('./routes/activityRoutes'); -const { canReadAttendance } = require('./policies/accessPolicy'); -const { recordActivity } = require('./services/activityService'); +const { createOriginsRouter } = require('./routes/originsRoutes'); +const { createFunnelsRouter } = require('./routes/funnelsRoutes'); +const { createSearchRouter } = require('./routes/searchRoutes'); +const { createAttendancesRouter } = require('./routes/attendancesRoutes'); +const { createApiKeysRouter } = require('./routes/apiKeysRoutes'); +const { createIntegrationsRouter } = require('./routes/integrationsRoutes'); const app = express(); @@ -82,602 +85,17 @@ apiRouter.use(createTenantsRouter({ pool, transporter, getBaseUrl })); apiRouter.use(createActivityRouter({ pool })); -// --- Origin Routes (Groups & Items) --- -apiRouter.get('/origins', async (req, res) => { - try { - const { tenantId } = req.query; - const effectiveTenantId = req.user.role === 'super_admin' ? tenantId : req.user.tenant_id; - if (!effectiveTenantId || effectiveTenantId === 'all') return res.json([]); +apiRouter.use(createOriginsRouter({ pool })); - const [groups] = await pool.query('SELECT * FROM origin_groups WHERE tenant_id = ? ORDER BY created_at ASC', [effectiveTenantId]); - - // Seed default origin group if none exists - if (groups.length === 0) { - const gid = `origrp_${crypto.randomUUID().split('-')[0]}`; - await pool.query('INSERT INTO origin_groups (id, tenant_id, name) VALUES (?, ?, ?)', [gid, effectiveTenantId, 'Origens Padrão']); - - const defaultOrigins = [ - { name: 'WhatsApp', color: 'bg-green-100 text-green-700 border-green-200 dark:bg-green-900/30 dark:text-green-400 dark:border-green-800' }, - { name: 'Instagram', color: 'bg-pink-100 text-pink-700 border-pink-200 dark:bg-pink-900/30 dark:text-pink-400 dark:border-pink-800' }, - { name: 'Website', color: 'bg-red-100 text-red-700 border-red-200 dark:bg-red-900/30 dark:text-red-400 dark:border-red-800' }, - { name: 'LinkedIn', color: 'bg-blue-100 text-blue-700 border-blue-200 dark:bg-blue-900/30 dark:text-blue-400 dark:border-blue-800' }, - { name: 'Indicação', color: 'bg-orange-100 text-orange-700 border-orange-200 dark:bg-orange-900/30 dark:text-orange-400 dark:border-orange-800' } - ]; - for (const origin of defaultOrigins) { - const oid = `oriitm_${crypto.randomUUID().split('-')[0]}`; - await pool.query( - 'INSERT INTO origin_items (id, origin_group_id, name, color_class) VALUES (?, ?, ?, ?)', - [oid, gid, origin.name, origin.color] - ); - } - - // Update all teams of this tenant to use this origin group if they have none - await pool.query('UPDATE teams SET origin_group_id = ? WHERE tenant_id = ? AND origin_group_id IS NULL', [gid, effectiveTenantId]); - - groups.push({ id: gid, tenant_id: effectiveTenantId, name: 'Origens Padrão' }); - } +apiRouter.use(createFunnelsRouter({ pool })); - const [items] = await pool.query('SELECT * FROM origin_items WHERE origin_group_id IN (?) ORDER BY created_at ASC', [groups.map(g => g.id)]); - const [teams] = await pool.query('SELECT id, origin_group_id FROM teams WHERE tenant_id = ? AND origin_group_id IS NOT NULL', [effectiveTenantId]); +apiRouter.use(createSearchRouter({ pool })); - const result = groups.map(g => ({ - ...g, - items: items.filter(i => i.origin_group_id === g.id), - teamIds: teams.filter(t => t.origin_group_id === g.id).map(t => t.id) - })); +apiRouter.use(createAttendancesRouter({ pool })); - res.json(result); - } catch (error) { - console.error("GET /origins error:", error); - res.status(500).json({ error: error.message }); - } -}); +apiRouter.use(createApiKeysRouter({ pool })); -apiRouter.post('/origins', requireRole(['admin', 'super_admin']), async (req, res) => { - const { name, tenantId } = req.body; - const effectiveTenantId = req.user.role === 'super_admin' ? tenantId : req.user.tenant_id; - try { - const gid = `origrp_${crypto.randomUUID().split('-')[0]}`; - await pool.query('INSERT INTO origin_groups (id, tenant_id, name) VALUES (?, ?, ?)', [gid, effectiveTenantId, name]); - res.status(201).json({ id: gid }); - } catch (error) { - res.status(500).json({ error: error.message }); - } -}); - -apiRouter.put('/origins/:id', requireRole(['admin', 'super_admin']), async (req, res) => { - const { name, teamIds } = req.body; - try { - if (name) { - await pool.query('UPDATE origin_groups SET name = ? WHERE id = ?', [name, req.params.id]); - } - if (teamIds && Array.isArray(teamIds)) { - await pool.query('UPDATE teams SET origin_group_id = NULL WHERE origin_group_id = ?', [req.params.id]); - if (teamIds.length > 0) { - await pool.query('UPDATE teams SET origin_group_id = ? WHERE id IN (?)', [req.params.id, teamIds]); - } - } - res.json({ message: 'Origin group updated.' }); - } catch (error) { - res.status(500).json({ error: error.message }); - } -}); - -apiRouter.delete('/origins/:id', requireRole(['admin', 'super_admin']), async (req, res) => { - try { - await pool.query('DELETE FROM origin_items WHERE origin_group_id = ?', [req.params.id]); - await pool.query('UPDATE teams SET origin_group_id = NULL WHERE origin_group_id = ?', [req.params.id]); - await pool.query('DELETE FROM origin_groups WHERE id = ?', [req.params.id]); - res.json({ message: 'Origin group deleted.' }); - } catch (error) { - res.status(500).json({ error: error.message }); - } -}); - -apiRouter.post('/origins/:id/items', requireRole(['admin', 'super_admin']), async (req, res) => { - const { name, color_class } = req.body; - try { - const oid = `oriitm_${crypto.randomUUID().split('-')[0]}`; - await pool.query( - 'INSERT INTO origin_items (id, origin_group_id, name, color_class) VALUES (?, ?, ?, ?)', - [oid, req.params.id, name, color_class || 'bg-zinc-100 text-zinc-800 border-zinc-200'] - ); - res.status(201).json({ id: oid }); - } catch (error) { - res.status(500).json({ error: error.message }); - } -}); - -apiRouter.put('/origin_items/:id', requireRole(['admin', 'super_admin']), async (req, res) => { - const { name, color_class } = req.body; - try { - const [existing] = await pool.query('SELECT * FROM origin_items WHERE id = ?', [req.params.id]); - if (existing.length === 0) return res.status(404).json({ error: 'Origin item not found' }); - - await pool.query('UPDATE origin_items SET name = ?, color_class = ? WHERE id = ?', [name || existing[0].name, color_class || existing[0].color_class, req.params.id]); - res.json({ message: 'Origin item updated.' }); - } catch (error) { - res.status(500).json({ error: error.message }); - } -}); - -apiRouter.delete('/origin_items/:id', requireRole(['admin', 'super_admin']), async (req, res) => { - try { - await pool.query('DELETE FROM origin_items WHERE id = ?', [req.params.id]); - res.json({ message: 'Origin item deleted.' }); - } catch (error) { - res.status(500).json({ error: error.message }); - } -}); - -// --- Funnel Routes --- -apiRouter.get('/funnels', async (req, res) => { - try { - const { tenantId } = req.query; - const effectiveTenantId = req.user.role === 'super_admin' ? tenantId : req.user.tenant_id; - if (!effectiveTenantId || effectiveTenantId === 'all') return res.json([]); - - const [funnels] = await pool.query('SELECT * FROM funnels WHERE tenant_id = ? ORDER BY created_at ASC', [effectiveTenantId]); - - // Seed default funnel if none exists - if (funnels.length === 0) { - const fid = `funnel_${crypto.randomUUID().split('-')[0]}`; - await pool.query('INSERT INTO funnels (id, tenant_id, name) VALUES (?, ?, ?)', [fid, effectiveTenantId, 'Funil Padrão']); - - const defaultStages = [ - { name: 'Sem atendimento', color: 'bg-zinc-100 text-zinc-700 border-zinc-200 dark:bg-dark-input dark:text-dark-muted dark:border-dark-border', order: 0 }, - { name: 'Identificação', color: 'bg-blue-100 text-blue-700 border-blue-200 dark:bg-blue-900/30 dark:text-blue-400 dark:border-blue-800', order: 1 }, - { name: 'Negociação', color: 'bg-purple-100 text-purple-700 border-purple-200 dark:bg-purple-900/30 dark:text-purple-400 dark:border-purple-800', order: 2 }, - { name: 'Ganhos', color: 'bg-green-100 text-green-700 border-green-200 dark:bg-green-900/30 dark:text-green-400 dark:border-green-800', order: 3 }, - { name: 'Perdidos', color: 'bg-red-100 text-red-700 border-red-200 dark:bg-red-900/30 dark:text-red-400 dark:border-red-800', order: 4 } - ]; - - for (const s of defaultStages) { - const sid = `stage_${crypto.randomUUID().split('-')[0]}`; - await pool.query( - 'INSERT INTO funnel_stages (id, funnel_id, name, color_class, order_index) VALUES (?, ?, ?, ?, ?)', - [sid, fid, s.name, s.color, s.order] - ); - } - - // Update all teams of this tenant to use this funnel if they have none - await pool.query('UPDATE teams SET funnel_id = ? WHERE tenant_id = ? AND funnel_id IS NULL', [fid, effectiveTenantId]); - - funnels.push({ id: fid, tenant_id: effectiveTenantId, name: 'Funil Padrão' }); - } - - const [stages] = await pool.query('SELECT * FROM funnel_stages WHERE funnel_id IN (?) ORDER BY order_index ASC', [funnels.map(f => f.id)]); - const [teams] = await pool.query('SELECT id, funnel_id FROM teams WHERE tenant_id = ? AND funnel_id IS NOT NULL', [effectiveTenantId]); - - const result = funnels.map(f => ({ - ...f, - stages: stages.filter(s => s.funnel_id === f.id), - teamIds: teams.filter(t => t.funnel_id === f.id).map(t => t.id) - })); - - res.json(result); - } catch (error) { - console.error("GET /funnels error:", error); - res.status(500).json({ error: error.message }); - } -}); - -apiRouter.post('/funnels', requireRole(['admin', 'super_admin']), async (req, res) => { - const { name, tenantId } = req.body; - const effectiveTenantId = req.user.role === 'super_admin' ? tenantId : req.user.tenant_id; - try { - const fid = `funnel_${crypto.randomUUID().split('-')[0]}`; - await pool.query('INSERT INTO funnels (id, tenant_id, name) VALUES (?, ?, ?)', [fid, effectiveTenantId, name]); - res.status(201).json({ id: fid }); - } catch (error) { - res.status(500).json({ error: error.message }); - } -}); - -apiRouter.put('/funnels/:id', requireRole(['admin', 'super_admin']), async (req, res) => { - const { name, teamIds } = req.body; - try { - if (name) { - await pool.query('UPDATE funnels SET name = ? WHERE id = ?', [name, req.params.id]); - } - if (teamIds && Array.isArray(teamIds)) { - await pool.query('UPDATE teams SET funnel_id = NULL WHERE funnel_id = ?', [req.params.id]); - if (teamIds.length > 0) { - await pool.query('UPDATE teams SET funnel_id = ? WHERE id IN (?)', [req.params.id, teamIds]); - } - } - res.json({ message: 'Funnel updated.' }); - } catch (error) { - res.status(500).json({ error: error.message }); - } -}); - -apiRouter.delete('/funnels/:id', requireRole(['admin', 'super_admin']), async (req, res) => { - try { - await pool.query('DELETE FROM funnel_stages WHERE funnel_id = ?', [req.params.id]); - await pool.query('UPDATE teams SET funnel_id = NULL WHERE funnel_id = ?', [req.params.id]); - await pool.query('DELETE FROM funnels WHERE id = ?', [req.params.id]); - res.json({ message: 'Funnel deleted.' }); - } catch (error) { - res.status(500).json({ error: error.message }); - } -}); - -apiRouter.post('/funnels/:id/stages', requireRole(['admin', 'super_admin']), async (req, res) => { - const { name, color_class, order_index } = req.body; - try { - const sid = `stage_${crypto.randomUUID().split('-')[0]}`; - await pool.query( - 'INSERT INTO funnel_stages (id, funnel_id, name, color_class, order_index) VALUES (?, ?, ?, ?, ?)', - [sid, req.params.id, name, color_class, order_index || 0] - ); - res.status(201).json({ id: sid }); - } catch (error) { - res.status(500).json({ error: error.message }); - } -}); - -apiRouter.put('/funnel_stages/:id', requireRole(['admin', 'super_admin']), async (req, res) => { - const { name, color_class, order_index } = req.body; - try { - const [existing] = await pool.query('SELECT * FROM funnel_stages WHERE id = ?', [req.params.id]); - if (existing.length === 0) return res.status(404).json({ error: 'Stage not found' }); - - await pool.query( - 'UPDATE funnel_stages SET name = ?, color_class = ?, order_index = ? WHERE id = ?', - [name || existing[0].name, color_class || existing[0].color_class, order_index !== undefined ? order_index : existing[0].order_index, req.params.id] - ); - res.json({ message: 'Stage updated.' }); - } catch (error) { - res.status(500).json({ error: error.message }); - } -}); - -apiRouter.delete('/funnel_stages/:id', requireRole(['admin', 'super_admin']), async (req, res) => { - try { - await pool.query('DELETE FROM funnel_stages WHERE id = ?', [req.params.id]); - res.json({ message: 'Stage deleted.' }); - } catch (error) { - res.status(500).json({ error: error.message }); - } -}); - -// --- Global Search --- -apiRouter.get('/search', async (req, res) => { - const { q } = req.query; - if (!q || q.length < 2) return res.json({ members: [], teams: [], attendances: [], organizations: [] }); - - const queryStr = `%${q}%`; - const results = { members: [], teams: [], attendances: [], organizations: [] }; - - try { - // 1. Search Members (only for roles above agent) - if (req.user.role !== 'agent') { - let membersQ = 'SELECT id, name, email, slug, role, team_id, avatar_url FROM users WHERE (name LIKE ? OR email LIKE ?)'; - const membersParams = [queryStr, queryStr]; - - if (req.user.role === 'super_admin') { - // No extra filters - } else if (req.user.role === 'admin') { - membersQ += ' AND tenant_id = ?'; - membersParams.push(req.user.tenant_id); - } else if (req.user.role === 'manager') { - membersQ += ' AND tenant_id = ? AND (team_id = ? OR id = ?)'; - membersParams.push(req.user.tenant_id, req.user.team_id, req.user.id); - } - const [members] = await pool.query(membersQ, membersParams); - results.members = members; - } - - // 2. Search Teams (only for roles above agent) - if (req.user.role !== 'agent') { - let teamsQ = 'SELECT id, name, description FROM teams WHERE name LIKE ?'; - const teamsParams = [queryStr]; - - if (req.user.role === 'super_admin') { - // No extra filters - } else if (req.user.role === 'admin') { - teamsQ += ' AND tenant_id = ?'; - teamsParams.push(req.user.tenant_id); - } else if (req.user.role === 'manager') { - teamsQ += ' AND tenant_id = ? AND id = ?'; - teamsParams.push(req.user.tenant_id, req.user.team_id); - } - const [teams] = await pool.query(teamsQ, teamsParams); - results.teams = teams; - } - - // 3. Search Organizations (only for super_admin) - if (req.user.role === 'super_admin') { - const [orgs] = await pool.query('SELECT id, name, slug, status FROM tenants WHERE name LIKE ? OR slug LIKE ? LIMIT 5', [queryStr, queryStr]); - results.organizations = orgs; - } - - // 4. Search Attendances - let attendancesQ = 'SELECT a.id, a.title, a.created_at, u.name as user_name FROM attendances a JOIN users u ON a.user_id = u.id WHERE a.title LIKE ?'; - const attendancesParams = [queryStr]; - - if (req.user.role === 'super_admin') { - // No extra filters - } else if (req.user.role === 'admin') { - attendancesQ += ' AND a.tenant_id = ?'; - attendancesParams.push(req.user.tenant_id); - } else if (req.user.role === 'manager') { - attendancesQ += ' AND a.tenant_id = ? AND u.team_id = ?'; - attendancesParams.push(req.user.tenant_id, req.user.team_id); - } else { - attendancesQ += ' AND a.user_id = ?'; - attendancesParams.push(req.user.id); - } - attendancesQ += ' LIMIT 10'; - const [attendances] = await pool.query(attendancesQ, attendancesParams); - results.attendances = attendances; - - res.json(results); - } catch (error) { - res.status(500).json({ error: error.message }); - } -}); - - -// --- Attendance Routes --- -apiRouter.get('/attendances', async (req, res) => { - try { - const { tenantId, userId, teamId, startDate, endDate, funnelStage, origin } = req.query; - const effectiveTenantId = req.user.role === 'super_admin' ? tenantId : req.user.tenant_id; - - 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)); } - - // 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 (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_') || userId.length === 36) { - q += ' AND a.user_id = ?'; - params.push(userId); - } else { - q += ' AND u.slug = ?'; - params.push(userId); - } - } } - - if (funnelStage && funnelStage !== 'all') { q += ' AND a.funnel_stage = ?'; params.push(funnelStage); } - if (origin && origin !== 'all') { q += ' AND a.origin = ?'; params.push(origin); } - - q += ' ORDER BY a.created_at DESC'; - const [rows] = await pool.query(q, params); - const processed = rows.map(r => ({ - ...r, - attention_points: typeof r.attention_points === 'string' ? JSON.parse(r.attention_points) : r.attention_points, - improvement_points: typeof r.improvement_points === 'string' ? JSON.parse(r.improvement_points) : r.improvement_points, - converted: Boolean(r.converted) - })); - res.json(processed); - } catch (error) { res.status(500).json({ error: error.message }); } -}); - -apiRouter.get('/attendances/:id', async (req, res) => { - try { - const [rows] = await pool.query( - 'SELECT a.*, u.team_id FROM attendances a JOIN users u ON a.user_id = u.id WHERE a.id = ?', - [req.params.id] - ); - if (rows.length === 0) return res.status(404).json({ error: 'Not found' }); - - if (!canReadAttendance(req.user, rows[0])) return res.status(403).json({ error: 'Acesso negado.' }); - - const r = rows[0]; - res.json({ - ...r, - attention_points: typeof r.attention_points === 'string' ? JSON.parse(r.attention_points) : r.attention_points, - improvement_points: typeof r.improvement_points === 'string' ? JSON.parse(r.improvement_points) : r.improvement_points, - converted: Boolean(r.converted) - }); - } catch (error) { res.status(500).json({ error: error.message }); } -}); - -// --- API Key Management Routes --- -apiRouter.get('/api-keys', requireRole(['admin', 'super_admin']), async (req, res) => { - try { - const { tenantId } = req.query; - const effectiveTenantId = req.user.role === 'super_admin' ? tenantId : req.user.tenant_id; - if (!effectiveTenantId || effectiveTenantId === 'all') return res.json([]); - - const [rows] = await pool.query( - 'SELECT id, name, created_at, last_used_at, CASE WHEN secret_key LIKE "masked:%" THEN CONCAT("fasto_sk_", RIGHT(secret_key, 6), "...") ELSE CONCAT(SUBSTRING(secret_key, 1, 14), "...") END as masked_key FROM api_keys WHERE tenant_id = ?', - [effectiveTenantId] - ); - res.json(rows); - } catch (error) { - res.status(500).json({ error: error.message }); - } -}); - -apiRouter.post('/api-keys', requireRole(['admin', 'super_admin']), async (req, res) => { - const { name, tenantId } = req.body; - const effectiveTenantId = req.user.role === 'super_admin' ? tenantId : req.user.tenant_id; - try { - const id = `apk_${crypto.randomUUID().split('-')[0]}`; - // Generate a strong, random 32-byte hex string for the secret key - const secretKey = `fasto_sk_${crypto.randomBytes(32).toString('hex')}`; - - await pool.query( - 'INSERT INTO api_keys (id, tenant_id, name, secret_key, secret_hash) VALUES (?, ?, ?, ?, ?)', - [id, effectiveTenantId, name || 'Nova Integração API', maskSecret(id, secretKey), hashSecret(secretKey)] - ); - - // We only return the actual secret key ONCE during creation. - res.status(201).json({ id, secret_key: secretKey, message: 'Chave criada. Salve-a agora, ela não será exibida novamente.' }); - } catch (error) { - res.status(500).json({ error: error.message }); - } -}); - -apiRouter.delete('/api-keys/:id', requireRole(['admin', 'super_admin']), async (req, res) => { - try { - const [existing] = await pool.query('SELECT tenant_id FROM api_keys WHERE id = ?', [req.params.id]); - if (existing.length === 0) return res.status(404).json({ error: 'Chave não encontrada' }); - if (req.user.role !== 'super_admin' && existing[0].tenant_id !== req.user.tenant_id) return res.status(403).json({ error: 'Acesso negado' }); - - await pool.query('DELETE FROM api_keys WHERE id = ?', [req.params.id]); - res.json({ message: 'Chave de API revogada com sucesso.' }); - } catch (error) { - res.status(500).json({ error: error.message }); - } -}); - -// --- External Integration API (n8n) --- -apiRouter.get('/integration/users', requireRole(['admin']), async (req, res) => { - if (!req.user.is_api_key) return res.status(403).json({ error: 'Endpoint restrito a chaves de API.' }); - try { - const [rows] = await pool.query( - 'SELECT u.id, u.name, u.email, t.name as team_name FROM users u LEFT JOIN teams t ON u.team_id = t.id WHERE u.tenant_id = ? AND u.status = "active"', - [req.user.tenant_id] - ); - res.json(rows); - } catch (error) { - res.status(500).json({ error: error.message }); - } -}); - -apiRouter.get('/integration/origins', requireRole(['admin']), async (req, res) => { - if (!req.user.is_api_key) return res.status(403).json({ error: 'Endpoint restrito a chaves de API.' }); - try { - const [groups] = await pool.query('SELECT id, name FROM origin_groups WHERE tenant_id = ?', [req.user.tenant_id]); - if (groups.length === 0) return res.json([]); - - const [items] = await pool.query('SELECT origin_group_id, name FROM origin_items WHERE origin_group_id IN (?) ORDER BY created_at ASC', [groups.map(g => g.id)]); - const [teams] = await pool.query('SELECT id as team_id, name as team_name, origin_group_id FROM teams WHERE tenant_id = ? AND origin_group_id IS NOT NULL', [req.user.tenant_id]); - - const result = groups.map(g => ({ - group_name: g.name, - origins: items.filter(i => i.origin_group_id === g.id).map(i => i.name), - assigned_teams: teams.filter(t => t.origin_group_id === g.id).map(t => ({ id: t.team_id, name: t.team_name })) - })); - - res.json(result); - } catch (error) { - res.status(500).json({ error: error.message }); - } -}); - -apiRouter.get('/integration/funnels', requireRole(['admin']), async (req, res) => { - if (!req.user.is_api_key) return res.status(403).json({ error: 'Endpoint restrito a chaves de API.' }); - try { - const [funnels] = await pool.query('SELECT id, name FROM funnels WHERE tenant_id = ?', [req.user.tenant_id]); - if (funnels.length === 0) return res.json([]); - - const [stages] = await pool.query('SELECT funnel_id, name, order_index FROM funnel_stages WHERE funnel_id IN (?) ORDER BY order_index ASC', [funnels.map(f => f.id)]); - const [teams] = await pool.query('SELECT id as team_id, name as team_name, funnel_id FROM teams WHERE tenant_id = ? AND funnel_id IS NOT NULL', [req.user.tenant_id]); - - const result = funnels.map(f => ({ - funnel_name: f.name, - stages: stages.filter(s => s.funnel_id === f.id).map(s => s.name), - assigned_teams: teams.filter(t => t.funnel_id === f.id).map(t => ({ id: t.team_id, name: t.team_name })) - })); - - res.json(result); - } catch (error) { - res.status(500).json({ error: error.message }); - } -}); - -apiRouter.post('/integration/attendances', requireRole(['admin']), async (req, res) => { - if (!req.user.is_api_key) return res.status(403).json({ error: 'Endpoint restrito a chaves de API.' }); - - const { - user_id, - origin, - funnel_stage, - title, - full_summary, - score, - first_response_time_min, - handling_time_min, - product_requested, - product_sold, - converted, - attention_points, - improvement_points - } = req.body; - - if (!user_id || !origin || !funnel_stage || !title) { - return res.status(400).json({ error: 'Campos obrigatórios ausentes: user_id, origin, funnel_stage, title' }); - } - - try { - // Validate user belongs to the API Key's tenant - const [users] = await pool.query('SELECT id FROM users WHERE id = ? AND tenant_id = ? AND status = "active"', [user_id, req.user.tenant_id]); - if (users.length === 0) return res.status(400).json({ error: 'user_id inválido, inativo ou não pertence a esta organização.' }); - - const attId = `att_${crypto.randomUUID().split('-')[0]}`; - await pool.query( - `INSERT INTO attendances ( - id, tenant_id, user_id, title, full_summary, score, - first_response_time_min, handling_time_min, - funnel_stage, origin, product_requested, product_sold, - converted, attention_points, improvement_points - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, - [ - attId, - req.user.tenant_id, - user_id, - title, - full_summary || null, - score || 0, - first_response_time_min || 0, - handling_time_min || 0, - funnel_stage, - origin, - product_requested || null, - product_sold || null, - converted ? 1 : 0, - attention_points ? JSON.stringify(attention_points) : null, - improvement_points ? JSON.stringify(improvement_points) : null - ] - ); - - // Automation Trigger: "Venda Fechada!" (Ganhos) - if (converted) { - // Find the user's manager/admin - const [managers] = await pool.query( - "SELECT id FROM users WHERE tenant_id = ? AND role IN ('admin', 'manager') AND id != ?", - [req.user.tenant_id, user_id] - ); - const [agentInfo] = await pool.query("SELECT name FROM users WHERE id = ?", [user_id]); - const agentName = agentInfo[0]?.name || 'Um agente'; - - for (const m of managers) { - await recordActivity(pool, { - userId: m.id, - type: 'success', - title: 'Venda Fechada!', - message: `${agentName} converteu um lead em ${funnel_stage}.`, - link: `/attendances/${attId}`, - }); - } - } - - res.status(201).json({ id: attId, message: 'Atendimento registrado com sucesso.' }); - } catch (error) { - console.error('Integration Error:', error); - res.status(500).json({ error: error.message }); - } -}); +apiRouter.use(createIntegrationsRouter({ pool })); // Mount the API Router app.use('/api', apiRouter); diff --git a/backend/routes/apiKeysRoutes.js b/backend/routes/apiKeysRoutes.js new file mode 100644 index 0000000..c430a18 --- /dev/null +++ b/backend/routes/apiKeysRoutes.js @@ -0,0 +1,61 @@ +const express = require('express'); +const crypto = require('crypto'); +const { requireRole } = require('../middleware/auth'); +const { hashSecret, maskSecret } = require('../utils/security'); + +const createApiKeysRouter = ({ pool }) => { + const router = express.Router(); + + router.get('/api-keys', requireRole(['admin', 'super_admin']), async (req, res) => { + try { + const { tenantId } = req.query; + const effectiveTenantId = req.user.role === 'super_admin' ? tenantId : req.user.tenant_id; + if (!effectiveTenantId || effectiveTenantId === 'all') return res.json([]); + + const [rows] = await pool.query( + 'SELECT id, name, created_at, last_used_at, CASE WHEN secret_key LIKE "masked:%" THEN CONCAT("fasto_sk_", RIGHT(secret_key, 6), "...") ELSE CONCAT(SUBSTRING(secret_key, 1, 14), "...") END as masked_key FROM api_keys WHERE tenant_id = ?', + [effectiveTenantId] + ); + res.json(rows); + } catch (error) { + res.status(500).json({ error: error.message }); + } + }); + + router.post('/api-keys', requireRole(['admin', 'super_admin']), async (req, res) => { + const { name, tenantId } = req.body; + const effectiveTenantId = req.user.role === 'super_admin' ? tenantId : req.user.tenant_id; + try { + const id = `apk_${crypto.randomUUID().split('-')[0]}`; + const secretKey = `fasto_sk_${crypto.randomBytes(32).toString('hex')}`; + + await pool.query( + 'INSERT INTO api_keys (id, tenant_id, name, secret_key, secret_hash) VALUES (?, ?, ?, ?, ?)', + [id, effectiveTenantId, name || 'Nova Integração API', maskSecret(id, secretKey), hashSecret(secretKey)] + ); + + res.status(201).json({ id, secret_key: secretKey, message: 'Chave criada. Salve-a agora, ela não será exibida novamente.' }); + } catch (error) { + res.status(500).json({ error: error.message }); + } + }); + + router.delete('/api-keys/:id', requireRole(['admin', 'super_admin']), async (req, res) => { + try { + const [existing] = await pool.query('SELECT tenant_id FROM api_keys WHERE id = ?', [req.params.id]); + if (existing.length === 0) return res.status(404).json({ error: 'Chave não encontrada' }); + if (req.user.role !== 'super_admin' && existing[0].tenant_id !== req.user.tenant_id) return res.status(403).json({ error: 'Acesso negado' }); + + await pool.query('DELETE FROM api_keys WHERE id = ?', [req.params.id]); + res.json({ message: 'Chave de API revogada com sucesso.' }); + } catch (error) { + res.status(500).json({ error: error.message }); + } + }); + + return router; +}; + +module.exports = { + createApiKeysRouter, +}; diff --git a/backend/routes/attendancesRoutes.js b/backend/routes/attendancesRoutes.js new file mode 100644 index 0000000..38f5b73 --- /dev/null +++ b/backend/routes/attendancesRoutes.js @@ -0,0 +1,88 @@ +const express = require('express'); +const { canReadAttendance } = require('../policies/accessPolicy'); + +const parseAttendance = (attendance) => ({ + ...attendance, + attention_points: typeof attendance.attention_points === 'string' ? JSON.parse(attendance.attention_points) : attendance.attention_points, + improvement_points: typeof attendance.improvement_points === 'string' ? JSON.parse(attendance.improvement_points) : attendance.improvement_points, + converted: Boolean(attendance.converted), +}); + +const createAttendancesRouter = ({ pool }) => { + const router = express.Router(); + + router.get('/attendances', async (req, res) => { + try { + const { tenantId, userId, teamId, startDate, endDate, funnelStage, origin } = req.query; + const effectiveTenantId = req.user.role === 'super_admin' ? tenantId : req.user.tenant_id; + + 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 (req.user.role === 'agent') { + q += ' AND a.user_id = ?'; + params.push(req.user.id); + } 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') { + if (userId.startsWith('u_') || userId.length === 36) { + q += ' AND a.user_id = ?'; + params.push(userId); + } else { + q += ' AND u.slug = ?'; + params.push(userId); + } + } + } + + if (funnelStage && funnelStage !== 'all') { + q += ' AND a.funnel_stage = ?'; + params.push(funnelStage); + } + if (origin && origin !== 'all') { + q += ' AND a.origin = ?'; + params.push(origin); + } + + q += ' ORDER BY a.created_at DESC'; + const [rows] = await pool.query(q, params); + res.json(rows.map(parseAttendance)); + } catch (error) { + res.status(500).json({ error: error.message }); + } + }); + + router.get('/attendances/:id', async (req, res) => { + try { + const [rows] = await pool.query( + 'SELECT a.*, u.team_id FROM attendances a JOIN users u ON a.user_id = u.id WHERE a.id = ?', + [req.params.id] + ); + if (rows.length === 0) return res.status(404).json({ error: 'Not found' }); + + if (!canReadAttendance(req.user, rows[0])) return res.status(403).json({ error: 'Acesso negado.' }); + + res.json(parseAttendance(rows[0])); + } catch (error) { + res.status(500).json({ error: error.message }); + } + }); + + return router; +}; + +module.exports = { + createAttendancesRouter, +}; diff --git a/backend/routes/funnelsRoutes.js b/backend/routes/funnelsRoutes.js new file mode 100644 index 0000000..8bc1587 --- /dev/null +++ b/backend/routes/funnelsRoutes.js @@ -0,0 +1,141 @@ +const express = require('express'); +const crypto = require('crypto'); +const { requireRole } = require('../middleware/auth'); + +const createFunnelsRouter = ({ pool }) => { + const router = express.Router(); + + router.get('/funnels', async (req, res) => { + try { + const { tenantId } = req.query; + const effectiveTenantId = req.user.role === 'super_admin' ? tenantId : req.user.tenant_id; + if (!effectiveTenantId || effectiveTenantId === 'all') return res.json([]); + + const [funnels] = await pool.query('SELECT * FROM funnels WHERE tenant_id = ? ORDER BY created_at ASC', [effectiveTenantId]); + + if (funnels.length === 0) { + const fid = `funnel_${crypto.randomUUID().split('-')[0]}`; + await pool.query('INSERT INTO funnels (id, tenant_id, name) VALUES (?, ?, ?)', [fid, effectiveTenantId, 'Funil Padrão']); + + const defaultStages = [ + { name: 'Sem atendimento', color: 'bg-zinc-100 text-zinc-700 border-zinc-200 dark:bg-dark-input dark:text-dark-muted dark:border-dark-border', order: 0 }, + { name: 'Identificação', color: 'bg-blue-100 text-blue-700 border-blue-200 dark:bg-blue-900/30 dark:text-blue-400 dark:border-blue-800', order: 1 }, + { name: 'Negociação', color: 'bg-purple-100 text-purple-700 border-purple-200 dark:bg-purple-900/30 dark:text-purple-400 dark:border-purple-800', order: 2 }, + { name: 'Ganhos', color: 'bg-green-100 text-green-700 border-green-200 dark:bg-green-900/30 dark:text-green-400 dark:border-green-800', order: 3 }, + { name: 'Perdidos', color: 'bg-red-100 text-red-700 border-red-200 dark:bg-red-900/30 dark:text-red-400 dark:border-red-800', order: 4 } + ]; + + for (const s of defaultStages) { + const sid = `stage_${crypto.randomUUID().split('-')[0]}`; + await pool.query( + 'INSERT INTO funnel_stages (id, funnel_id, name, color_class, order_index) VALUES (?, ?, ?, ?, ?)', + [sid, fid, s.name, s.color, s.order] + ); + } + + await pool.query('UPDATE teams SET funnel_id = ? WHERE tenant_id = ? AND funnel_id IS NULL', [fid, effectiveTenantId]); + funnels.push({ id: fid, tenant_id: effectiveTenantId, name: 'Funil Padrão' }); + } + + const [stages] = await pool.query('SELECT * FROM funnel_stages WHERE funnel_id IN (?) ORDER BY order_index ASC', [funnels.map(f => f.id)]); + const [teams] = await pool.query('SELECT id, funnel_id FROM teams WHERE tenant_id = ? AND funnel_id IS NOT NULL', [effectiveTenantId]); + + const result = funnels.map(f => ({ + ...f, + stages: stages.filter(s => s.funnel_id === f.id), + teamIds: teams.filter(t => t.funnel_id === f.id).map(t => t.id) + })); + + res.json(result); + } catch (error) { + console.error("GET /funnels error:", error); + res.status(500).json({ error: error.message }); + } + }); + + router.post('/funnels', requireRole(['admin', 'super_admin']), async (req, res) => { + const { name, tenantId } = req.body; + const effectiveTenantId = req.user.role === 'super_admin' ? tenantId : req.user.tenant_id; + try { + const fid = `funnel_${crypto.randomUUID().split('-')[0]}`; + await pool.query('INSERT INTO funnels (id, tenant_id, name) VALUES (?, ?, ?)', [fid, effectiveTenantId, name]); + res.status(201).json({ id: fid }); + } catch (error) { + res.status(500).json({ error: error.message }); + } + }); + + router.put('/funnels/:id', requireRole(['admin', 'super_admin']), async (req, res) => { + const { name, teamIds } = req.body; + try { + if (name) { + await pool.query('UPDATE funnels SET name = ? WHERE id = ?', [name, req.params.id]); + } + if (teamIds && Array.isArray(teamIds)) { + await pool.query('UPDATE teams SET funnel_id = NULL WHERE funnel_id = ?', [req.params.id]); + if (teamIds.length > 0) { + await pool.query('UPDATE teams SET funnel_id = ? WHERE id IN (?)', [req.params.id, teamIds]); + } + } + res.json({ message: 'Funnel updated.' }); + } catch (error) { + res.status(500).json({ error: error.message }); + } + }); + + router.delete('/funnels/:id', requireRole(['admin', 'super_admin']), async (req, res) => { + try { + await pool.query('DELETE FROM funnel_stages WHERE funnel_id = ?', [req.params.id]); + await pool.query('UPDATE teams SET funnel_id = NULL WHERE funnel_id = ?', [req.params.id]); + await pool.query('DELETE FROM funnels WHERE id = ?', [req.params.id]); + res.json({ message: 'Funnel deleted.' }); + } catch (error) { + res.status(500).json({ error: error.message }); + } + }); + + router.post('/funnels/:id/stages', requireRole(['admin', 'super_admin']), async (req, res) => { + const { name, color_class, order_index } = req.body; + try { + const sid = `stage_${crypto.randomUUID().split('-')[0]}`; + await pool.query( + 'INSERT INTO funnel_stages (id, funnel_id, name, color_class, order_index) VALUES (?, ?, ?, ?, ?)', + [sid, req.params.id, name, color_class, order_index || 0] + ); + res.status(201).json({ id: sid }); + } catch (error) { + res.status(500).json({ error: error.message }); + } + }); + + router.put('/funnel_stages/:id', requireRole(['admin', 'super_admin']), async (req, res) => { + const { name, color_class, order_index } = req.body; + try { + const [existing] = await pool.query('SELECT * FROM funnel_stages WHERE id = ?', [req.params.id]); + if (existing.length === 0) return res.status(404).json({ error: 'Stage not found' }); + + await pool.query( + 'UPDATE funnel_stages SET name = ?, color_class = ?, order_index = ? WHERE id = ?', + [name || existing[0].name, color_class || existing[0].color_class, order_index !== undefined ? order_index : existing[0].order_index, req.params.id] + ); + res.json({ message: 'Stage updated.' }); + } catch (error) { + res.status(500).json({ error: error.message }); + } + }); + + router.delete('/funnel_stages/:id', requireRole(['admin', 'super_admin']), async (req, res) => { + try { + await pool.query('DELETE FROM funnel_stages WHERE id = ?', [req.params.id]); + res.json({ message: 'Stage deleted.' }); + } catch (error) { + res.status(500).json({ error: error.message }); + } + }); + + return router; +}; + +module.exports = { + createFunnelsRouter, +}; diff --git a/backend/routes/integrationsRoutes.js b/backend/routes/integrationsRoutes.js new file mode 100644 index 0000000..7269f89 --- /dev/null +++ b/backend/routes/integrationsRoutes.js @@ -0,0 +1,155 @@ +const express = require('express'); +const crypto = require('crypto'); +const { requireRole } = require('../middleware/auth'); +const { recordActivity } = require('../services/activityService'); + +const requireApiKey = (req, res) => { + if (req.user.is_api_key) return true; + res.status(403).json({ error: 'Endpoint restrito a chaves de API.' }); + return false; +}; + +const createIntegrationsRouter = ({ pool }) => { + const router = express.Router(); + + router.get('/integration/users', requireRole(['admin']), async (req, res) => { + if (!requireApiKey(req, res)) return; + try { + const [rows] = await pool.query( + 'SELECT u.id, u.name, u.email, t.name as team_name FROM users u LEFT JOIN teams t ON u.team_id = t.id WHERE u.tenant_id = ? AND u.status = "active"', + [req.user.tenant_id] + ); + res.json(rows); + } catch (error) { + res.status(500).json({ error: error.message }); + } + }); + + router.get('/integration/origins', requireRole(['admin']), async (req, res) => { + if (!requireApiKey(req, res)) return; + try { + const [groups] = await pool.query('SELECT id, name FROM origin_groups WHERE tenant_id = ?', [req.user.tenant_id]); + if (groups.length === 0) return res.json([]); + + const [items] = await pool.query('SELECT origin_group_id, name FROM origin_items WHERE origin_group_id IN (?) ORDER BY created_at ASC', [groups.map(g => g.id)]); + const [teams] = await pool.query('SELECT id as team_id, name as team_name, origin_group_id FROM teams WHERE tenant_id = ? AND origin_group_id IS NOT NULL', [req.user.tenant_id]); + + const result = groups.map(g => ({ + group_name: g.name, + origins: items.filter(i => i.origin_group_id === g.id).map(i => i.name), + assigned_teams: teams.filter(t => t.origin_group_id === g.id).map(t => ({ id: t.team_id, name: t.team_name })) + })); + + res.json(result); + } catch (error) { + res.status(500).json({ error: error.message }); + } + }); + + router.get('/integration/funnels', requireRole(['admin']), async (req, res) => { + if (!requireApiKey(req, res)) return; + try { + const [funnels] = await pool.query('SELECT id, name FROM funnels WHERE tenant_id = ?', [req.user.tenant_id]); + if (funnels.length === 0) return res.json([]); + + const [stages] = await pool.query('SELECT funnel_id, name, order_index FROM funnel_stages WHERE funnel_id IN (?) ORDER BY order_index ASC', [funnels.map(f => f.id)]); + const [teams] = await pool.query('SELECT id as team_id, name as team_name, funnel_id FROM teams WHERE tenant_id = ? AND funnel_id IS NOT NULL', [req.user.tenant_id]); + + const result = funnels.map(f => ({ + funnel_name: f.name, + stages: stages.filter(s => s.funnel_id === f.id).map(s => s.name), + assigned_teams: teams.filter(t => t.funnel_id === f.id).map(t => ({ id: t.team_id, name: t.team_name })) + })); + + res.json(result); + } catch (error) { + res.status(500).json({ error: error.message }); + } + }); + + router.post('/integration/attendances', requireRole(['admin']), async (req, res) => { + if (!requireApiKey(req, res)) return; + + const { + user_id, + origin, + funnel_stage, + title, + full_summary, + score, + first_response_time_min, + handling_time_min, + product_requested, + product_sold, + converted, + attention_points, + improvement_points + } = req.body; + + if (!user_id || !origin || !funnel_stage || !title) { + return res.status(400).json({ error: 'Campos obrigatórios ausentes: user_id, origin, funnel_stage, title' }); + } + + try { + const [users] = await pool.query('SELECT id FROM users WHERE id = ? AND tenant_id = ? AND status = "active"', [user_id, req.user.tenant_id]); + if (users.length === 0) return res.status(400).json({ error: 'user_id inválido, inativo ou não pertence a esta organização.' }); + + const attId = `att_${crypto.randomUUID().split('-')[0]}`; + await pool.query( + `INSERT INTO attendances ( + id, tenant_id, user_id, title, full_summary, score, + first_response_time_min, handling_time_min, + funnel_stage, origin, product_requested, product_sold, + converted, attention_points, improvement_points + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + [ + attId, + req.user.tenant_id, + user_id, + title, + full_summary || null, + score || 0, + first_response_time_min || 0, + handling_time_min || 0, + funnel_stage, + origin, + product_requested || null, + product_sold || null, + converted ? 1 : 0, + attention_points ? JSON.stringify(attention_points) : null, + improvement_points ? JSON.stringify(improvement_points) : null + ] + ); + + if (converted) { + const [managers] = await pool.query( + "SELECT id FROM users WHERE tenant_id = ? AND role IN ('admin', 'manager') AND id != ?", + [req.user.tenant_id, user_id] + ); + const [agentInfo] = await pool.query("SELECT name FROM users WHERE id = ?", [user_id]); + const agentName = agentInfo[0]?.name || 'Um agente'; + + for (const m of managers) { + await recordActivity(pool, { + userId: m.id, + type: 'success', + title: 'Venda Fechada!', + message: `${agentName} converteu um lead em ${funnel_stage}.`, + link: `/attendances/${attId}`, + }); + } + } + + res.status(201).json({ id: attId, message: 'Atendimento registrado com sucesso.' }); + } catch (error) { + console.error('Integration Error:', error); + res.status(500).json({ error: error.message }); + } + }); + + return router; +}; + +module.exports = { + createIntegrationsRouter, +}; diff --git a/backend/routes/originsRoutes.js b/backend/routes/originsRoutes.js new file mode 100644 index 0000000..c20e1f1 --- /dev/null +++ b/backend/routes/originsRoutes.js @@ -0,0 +1,138 @@ +const express = require('express'); +const crypto = require('crypto'); +const { requireRole } = require('../middleware/auth'); + +const createOriginsRouter = ({ pool }) => { + const router = express.Router(); + + router.get('/origins', async (req, res) => { + try { + const { tenantId } = req.query; + const effectiveTenantId = req.user.role === 'super_admin' ? tenantId : req.user.tenant_id; + if (!effectiveTenantId || effectiveTenantId === 'all') return res.json([]); + + const [groups] = await pool.query('SELECT * FROM origin_groups WHERE tenant_id = ? ORDER BY created_at ASC', [effectiveTenantId]); + + if (groups.length === 0) { + const gid = `origrp_${crypto.randomUUID().split('-')[0]}`; + await pool.query('INSERT INTO origin_groups (id, tenant_id, name) VALUES (?, ?, ?)', [gid, effectiveTenantId, 'Origens Padrão']); + + const defaultOrigins = [ + { name: 'WhatsApp', color: 'bg-green-100 text-green-700 border-green-200 dark:bg-green-900/30 dark:text-green-400 dark:border-green-800' }, + { name: 'Instagram', color: 'bg-pink-100 text-pink-700 border-pink-200 dark:bg-pink-900/30 dark:text-pink-400 dark:border-pink-800' }, + { name: 'Website', color: 'bg-red-100 text-red-700 border-red-200 dark:bg-red-900/30 dark:text-red-400 dark:border-red-800' }, + { name: 'LinkedIn', color: 'bg-blue-100 text-blue-700 border-blue-200 dark:bg-blue-900/30 dark:text-blue-400 dark:border-blue-800' }, + { name: 'Indicação', color: 'bg-orange-100 text-orange-700 border-orange-200 dark:bg-orange-900/30 dark:text-orange-400 dark:border-orange-800' } + ]; + + for (const origin of defaultOrigins) { + const oid = `oriitm_${crypto.randomUUID().split('-')[0]}`; + await pool.query( + 'INSERT INTO origin_items (id, origin_group_id, name, color_class) VALUES (?, ?, ?, ?)', + [oid, gid, origin.name, origin.color] + ); + } + + await pool.query('UPDATE teams SET origin_group_id = ? WHERE tenant_id = ? AND origin_group_id IS NULL', [gid, effectiveTenantId]); + groups.push({ id: gid, tenant_id: effectiveTenantId, name: 'Origens Padrão' }); + } + + const [items] = await pool.query('SELECT * FROM origin_items WHERE origin_group_id IN (?) ORDER BY created_at ASC', [groups.map(g => g.id)]); + const [teams] = await pool.query('SELECT id, origin_group_id FROM teams WHERE tenant_id = ? AND origin_group_id IS NOT NULL', [effectiveTenantId]); + + const result = groups.map(g => ({ + ...g, + items: items.filter(i => i.origin_group_id === g.id), + teamIds: teams.filter(t => t.origin_group_id === g.id).map(t => t.id) + })); + + res.json(result); + } catch (error) { + console.error("GET /origins error:", error); + res.status(500).json({ error: error.message }); + } + }); + + router.post('/origins', requireRole(['admin', 'super_admin']), async (req, res) => { + const { name, tenantId } = req.body; + const effectiveTenantId = req.user.role === 'super_admin' ? tenantId : req.user.tenant_id; + try { + const gid = `origrp_${crypto.randomUUID().split('-')[0]}`; + await pool.query('INSERT INTO origin_groups (id, tenant_id, name) VALUES (?, ?, ?)', [gid, effectiveTenantId, name]); + res.status(201).json({ id: gid }); + } catch (error) { + res.status(500).json({ error: error.message }); + } + }); + + router.put('/origins/:id', requireRole(['admin', 'super_admin']), async (req, res) => { + const { name, teamIds } = req.body; + try { + if (name) { + await pool.query('UPDATE origin_groups SET name = ? WHERE id = ?', [name, req.params.id]); + } + if (teamIds && Array.isArray(teamIds)) { + await pool.query('UPDATE teams SET origin_group_id = NULL WHERE origin_group_id = ?', [req.params.id]); + if (teamIds.length > 0) { + await pool.query('UPDATE teams SET origin_group_id = ? WHERE id IN (?)', [req.params.id, teamIds]); + } + } + res.json({ message: 'Origin group updated.' }); + } catch (error) { + res.status(500).json({ error: error.message }); + } + }); + + router.delete('/origins/:id', requireRole(['admin', 'super_admin']), async (req, res) => { + try { + await pool.query('DELETE FROM origin_items WHERE origin_group_id = ?', [req.params.id]); + await pool.query('UPDATE teams SET origin_group_id = NULL WHERE origin_group_id = ?', [req.params.id]); + await pool.query('DELETE FROM origin_groups WHERE id = ?', [req.params.id]); + res.json({ message: 'Origin group deleted.' }); + } catch (error) { + res.status(500).json({ error: error.message }); + } + }); + + router.post('/origins/:id/items', requireRole(['admin', 'super_admin']), async (req, res) => { + const { name, color_class } = req.body; + try { + const oid = `oriitm_${crypto.randomUUID().split('-')[0]}`; + await pool.query( + 'INSERT INTO origin_items (id, origin_group_id, name, color_class) VALUES (?, ?, ?, ?)', + [oid, req.params.id, name, color_class || 'bg-zinc-100 text-zinc-800 border-zinc-200'] + ); + res.status(201).json({ id: oid }); + } catch (error) { + res.status(500).json({ error: error.message }); + } + }); + + router.put('/origin_items/:id', requireRole(['admin', 'super_admin']), async (req, res) => { + const { name, color_class } = req.body; + try { + const [existing] = await pool.query('SELECT * FROM origin_items WHERE id = ?', [req.params.id]); + if (existing.length === 0) return res.status(404).json({ error: 'Origin item not found' }); + + await pool.query('UPDATE origin_items SET name = ?, color_class = ? WHERE id = ?', [name || existing[0].name, color_class || existing[0].color_class, req.params.id]); + res.json({ message: 'Origin item updated.' }); + } catch (error) { + res.status(500).json({ error: error.message }); + } + }); + + router.delete('/origin_items/:id', requireRole(['admin', 'super_admin']), async (req, res) => { + try { + await pool.query('DELETE FROM origin_items WHERE id = ?', [req.params.id]); + res.json({ message: 'Origin item deleted.' }); + } catch (error) { + res.status(500).json({ error: error.message }); + } + }); + + return router; +}; + +module.exports = { + createOriginsRouter, +}; diff --git a/backend/routes/searchRoutes.js b/backend/routes/searchRoutes.js new file mode 100644 index 0000000..39601fb --- /dev/null +++ b/backend/routes/searchRoutes.js @@ -0,0 +1,80 @@ +const express = require('express'); + +const createSearchRouter = ({ pool }) => { + const router = express.Router(); + + router.get('/search', async (req, res) => { + const { q } = req.query; + if (!q || q.length < 2) return res.json({ members: [], teams: [], attendances: [], organizations: [] }); + + const queryStr = `%${q}%`; + const results = { members: [], teams: [], attendances: [], organizations: [] }; + + try { + if (req.user.role !== 'agent') { + let membersQ = 'SELECT id, name, email, slug, role, team_id, avatar_url FROM users WHERE (name LIKE ? OR email LIKE ?)'; + const membersParams = [queryStr, queryStr]; + + if (req.user.role === 'admin') { + membersQ += ' AND tenant_id = ?'; + membersParams.push(req.user.tenant_id); + } else if (req.user.role === 'manager') { + membersQ += ' AND tenant_id = ? AND (team_id = ? OR id = ?)'; + membersParams.push(req.user.tenant_id, req.user.team_id, req.user.id); + } + + const [members] = await pool.query(membersQ, membersParams); + results.members = members; + } + + if (req.user.role !== 'agent') { + let teamsQ = 'SELECT id, name, description FROM teams WHERE name LIKE ?'; + const teamsParams = [queryStr]; + + if (req.user.role === 'admin') { + teamsQ += ' AND tenant_id = ?'; + teamsParams.push(req.user.tenant_id); + } else if (req.user.role === 'manager') { + teamsQ += ' AND tenant_id = ? AND id = ?'; + teamsParams.push(req.user.tenant_id, req.user.team_id); + } + + const [teams] = await pool.query(teamsQ, teamsParams); + results.teams = teams; + } + + if (req.user.role === 'super_admin') { + const [orgs] = await pool.query('SELECT id, name, slug, status FROM tenants WHERE name LIKE ? OR slug LIKE ? LIMIT 5', [queryStr, queryStr]); + results.organizations = orgs; + } + + let attendancesQ = 'SELECT a.id, a.title, a.created_at, u.name as user_name FROM attendances a JOIN users u ON a.user_id = u.id WHERE a.title LIKE ?'; + const attendancesParams = [queryStr]; + + if (req.user.role === 'admin') { + attendancesQ += ' AND a.tenant_id = ?'; + attendancesParams.push(req.user.tenant_id); + } else if (req.user.role === 'manager') { + attendancesQ += ' AND a.tenant_id = ? AND u.team_id = ?'; + attendancesParams.push(req.user.tenant_id, req.user.team_id); + } else if (req.user.role !== 'super_admin') { + attendancesQ += ' AND a.user_id = ?'; + attendancesParams.push(req.user.id); + } + + attendancesQ += ' LIMIT 10'; + const [attendances] = await pool.query(attendancesQ, attendancesParams); + results.attendances = attendances; + + res.json(results); + } catch (error) { + res.status(500).json({ error: error.message }); + } + }); + + return router; +}; + +module.exports = { + createSearchRouter, +};