Extract remaining backend routes

This commit is contained in:
Cauê Faleiros
2026-05-29 14:42:07 -03:00
parent e64654c820
commit 871d1eb850
7 changed files with 676 additions and 595 deletions

View File

@@ -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);