feat: implement relational lead origins with team assignments
All checks were successful
Build and Deploy / build-and-push (push) Successful in 1m51s
All checks were successful
Build and Deploy / build-and-push (push) Successful in 1m51s
- Dropped simple origins table in favor of origin_groups and origin_items to match the Funnels architecture. - Added origin_group_id to teams table to assign specific origins to specific teams. - Updated /admin/origins page to support creating origin groups, adding origin items to them, and assigning teams to groups. - Updated Dashboard and UserDetail pages to dynamically load the exact origin items belonging to the active team/user.
This commit is contained in:
188
backend/index.js
188
backend/index.js
@@ -612,6 +612,128 @@ apiRouter.delete('/notifications/:id', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// --- 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([]);
|
||||
|
||||
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 = ['WhatsApp', 'Instagram', 'Website', 'LinkedIn', 'Indicação'];
|
||||
for (const name of defaultOrigins) {
|
||||
const oid = `oriitm_${crypto.randomUUID().split('-')[0]}`;
|
||||
await pool.query(
|
||||
'INSERT INTO origin_items (id, origin_group_id, name) VALUES (?, ?, ?)',
|
||||
[oid, gid, name]
|
||||
);
|
||||
}
|
||||
|
||||
// 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' });
|
||||
}
|
||||
|
||||
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 });
|
||||
}
|
||||
});
|
||||
|
||||
apiRouter.post('/origins', requireRole(['admin', 'owner', 'manager', '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', 'owner', 'manager', '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', 'owner', 'manager', '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', 'owner', 'manager', 'super_admin']), async (req, res) => {
|
||||
const { name } = req.body;
|
||||
try {
|
||||
const oid = `oriitm_${crypto.randomUUID().split('-')[0]}`;
|
||||
await pool.query(
|
||||
'INSERT INTO origin_items (id, origin_group_id, name) VALUES (?, ?, ?)',
|
||||
[oid, req.params.id, name]
|
||||
);
|
||||
res.status(201).json({ id: oid });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
apiRouter.put('/origin_items/:id', requireRole(['admin', 'owner', 'manager', 'super_admin']), async (req, res) => {
|
||||
const { name } = 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 = ? WHERE id = ?', [name || existing[0].name, 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', 'owner', 'manager', '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 {
|
||||
@@ -952,6 +1074,37 @@ apiRouter.get('/integration/users', requireRole(['admin']), async (req, res) =>
|
||||
}
|
||||
});
|
||||
|
||||
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 [origins] = await pool.query('SELECT name FROM origins WHERE tenant_id = ? ORDER BY created_at ASC', [req.user.tenant_id]);
|
||||
res.json(origins.map(o => o.name));
|
||||
} 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.' });
|
||||
|
||||
@@ -1288,9 +1441,9 @@ const provisionSuperAdmin = async (retries = 10, delay = 10000) => {
|
||||
if (err.code !== 'ER_DUP_FIELDNAME') console.log('Schema update note (sound_enabled):', err.message);
|
||||
}
|
||||
|
||||
// Update origin enum
|
||||
// Update origin to VARCHAR for custom origins
|
||||
try {
|
||||
await connection.query("ALTER TABLE attendances MODIFY COLUMN origin ENUM('WhatsApp','Instagram','Website','LinkedIn','Indicação') NOT NULL");
|
||||
await connection.query("ALTER TABLE attendances MODIFY COLUMN origin VARCHAR(255) NOT NULL");
|
||||
} catch (err) {
|
||||
console.log('Schema update note (origin):', err.message);
|
||||
}
|
||||
@@ -1309,6 +1462,37 @@ const provisionSuperAdmin = async (retries = 10, delay = 10000) => {
|
||||
if (err.code !== 'ER_DUP_FIELDNAME') console.log('Schema update note (full_summary):', err.message);
|
||||
}
|
||||
|
||||
// Create origin_groups table
|
||||
await connection.query(`
|
||||
CREATE TABLE IF NOT EXISTS origin_groups (
|
||||
id varchar(36) NOT NULL,
|
||||
tenant_id varchar(36) NOT NULL,
|
||||
name varchar(255) NOT NULL,
|
||||
created_at timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id),
|
||||
KEY tenant_id (tenant_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
`);
|
||||
|
||||
// Create origin_items table
|
||||
await connection.query(`
|
||||
CREATE TABLE IF NOT EXISTS origin_items (
|
||||
id varchar(36) NOT NULL,
|
||||
origin_group_id varchar(36) NOT NULL,
|
||||
name varchar(255) NOT NULL,
|
||||
created_at timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id),
|
||||
KEY origin_group_id (origin_group_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
`);
|
||||
|
||||
// Add origin_group_id to teams
|
||||
try {
|
||||
await connection.query("ALTER TABLE teams ADD COLUMN origin_group_id VARCHAR(36) DEFAULT NULL");
|
||||
} catch (err) {
|
||||
if (err.code !== 'ER_DUP_FIELDNAME') console.log('Schema update note (teams.origin_group_id):', err.message);
|
||||
}
|
||||
|
||||
// Rename summary to title
|
||||
try {
|
||||
await connection.query("ALTER TABLE attendances RENAME COLUMN summary TO title");
|
||||
|
||||
Reference in New Issue
Block a user