diff --git a/backend/index.js b/backend/index.js index db523c7..8c5ac19 100644 --- a/backend/index.js +++ b/backend/index.js @@ -993,7 +993,7 @@ apiRouter.get('/search', async (req, res) => { // 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 ?', [queryStr, queryStr]); + 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; } @@ -1013,6 +1013,7 @@ apiRouter.get('/search', async (req, res) => { attendancesQ += ' AND a.user_id = ?'; attendancesParams.push(req.user.id); } + attendancesQ += ' LIMIT 10'; const [attendances] = await pool.query(attendancesQ, attendancesParams); results.attendances = attendances; @@ -1047,17 +1048,16 @@ apiRouter.get('/attendances', async (req, res) => { params.push(teamId); } - if (userId && userId !== 'all') { + if (userId && userId !== 'all') { // check if it's a slug or id - if (userId.startsWith('u_')) { - q += ' AND a.user_id = ?'; - params.push(userId); + 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); } diff --git a/backend/seed_data.js b/backend/seed_data.js new file mode 100644 index 0000000..ed88c8e --- /dev/null +++ b/backend/seed_data.js @@ -0,0 +1,184 @@ +const pool = require('./db.js'); +const crypto = require('crypto'); + +const PRODUCTS = [ + "Pneu Aro 13 175/70R13", + "Pneu Aro 14 175/70R14", + "Pneu Aro 15 195/60R15", + "Pneu Aro 16 205/55R16", + "Pneu Aro 17 225/45R17", + "Alinhamento e Balanceamento", + "Revisão do Sistema de Arrefecimento", + "Manutenção de Freios", + "Troca de Óleo e Filtros", + "Limpeza de Bico" +]; + +const DEFAULT_ORIGINS = ['WhatsApp', 'Instagram', 'Website', 'LinkedIn', 'Indicação']; +const DEFAULT_FUNNELS = ['Sem atendimento', 'Identificação', 'Negociação', 'Ganhos', 'Perdidos']; + +const FIRST_NAMES = ["Ana", "Bruno", "Carlos", "Daniela", "Eduardo", "Fernanda", "Gabriel", "Helena", "Igor", "Julia", "Lucas", "Mariana", "Nicolas", "Olivia", "Pedro", "Quintino", "Rafael", "Sofia", "Thiago", "Ursula", "Victor", "Wagner", "Xuxa", "Yuri", "Zeca", "Amanda", "Beto", "Camila", "Diogo", "Elisa", "Fabio", "Gisele", "Henrique", "Isabela", "Joao", "Karla"]; +const LAST_NAMES = ["Silva", "Santos", "Oliveira", "Souza", "Rodrigues", "Ferreira", "Alves", "Pereira", "Lima", "Gomes", "Costa", "Ribeiro", "Martins", "Carvalho", "Almeida", "Lopes", "Soares", "Fernandes", "Vieira", "Barbosa"]; + +function getRandomItem(arr) { + return arr[Math.floor(Math.random() * arr.length)]; +} + +function getRandomInt(min, max) { + return Math.floor(Math.random() * (max - min + 1)) + min; +} + +function generateUniqueNames(count) { + const names = new Set(); + while (names.size < count) { + names.add(`${getRandomItem(FIRST_NAMES)} ${getRandomItem(LAST_NAMES)}`); + } + return Array.from(names); +} + +async function run() { + try { + console.log('🔄 Iniciando geração de dados...'); + + // 1. Encontrar o tenant "teste" + const [tenants] = await pool.query(`SELECT id FROM tenants WHERE slug = 'teste' OR name LIKE '%teste%' LIMIT 1`); + if (tenants.length === 0) { + console.log('❌ Tenant "teste" não encontrado.'); + process.exit(1); + } + const tenantId = tenants[0].id; + console.log(`✅ Tenant "teste" encontrado: ${tenantId}`); + + // Pegar origens e funis dinâmicos se existirem + let origins = [...DEFAULT_ORIGINS]; + let funnels = [...DEFAULT_FUNNELS]; + let originGroupId = null; + let funnelId = null; + + try { + const [originGroups] = await pool.query(`SELECT id FROM origin_groups WHERE tenant_id = ? LIMIT 1`, [tenantId]); + if (originGroups.length > 0) { + originGroupId = originGroups[0].id; + const [originItems] = await pool.query(`SELECT name FROM origin_items WHERE origin_group_id = ?`, [originGroupId]); + if (originItems.length > 0) origins = originItems.map(o => o.name); + } + const [funnelGroups] = await pool.query(`SELECT id FROM funnels WHERE tenant_id = ? LIMIT 1`, [tenantId]); + if (funnelGroups.length > 0) { + funnelId = funnelGroups[0].id; + const [funnelStages] = await pool.query(`SELECT name FROM funnel_stages WHERE funnel_id = ?`, [funnelId]); + if (funnelStages.length > 0) funnels = funnelStages.map(f => f.name); + } + } catch (e) { + console.log('Aviso: Usando origens/funis padrão devido a erro na busca de dinâmicos.'); + } + + // 2. Limpar dados existentes do tenant (exceto admin e tenant em si) + console.log('🧹 Limpando attendances antigas...'); + await pool.query(`DELETE FROM attendances WHERE tenant_id = ?`, [tenantId]); + + console.log('🧹 Limpando usuários antigos (exceto admin)...'); + await pool.query(`DELETE FROM users WHERE tenant_id = ? AND role != 'admin'`, [tenantId]); + + console.log('🧹 Limpando times antigos...'); + await pool.query(`DELETE FROM teams WHERE tenant_id = ?`, [tenantId]); + + // 3. Criar 5 times + const teams = []; + for (let i = 1; i <= 5; i++) { + const teamId = crypto.randomUUID(); + await pool.query(`INSERT INTO teams (id, tenant_id, name, description, origin_group_id, funnel_id) VALUES (?, ?, ?, ?, ?, ?)`, + [teamId, tenantId, `Equipe Vendas ${i}`, `Equipe responsável pela região ${i}`, originGroupId, funnelId]); + teams.push(teamId); + } + console.log('✅ 5 times criados.'); + + // 4. Criar 36 usuários (1 manager por time = 5 managers, 31 agents) + const users = []; + const roles = ['manager', 'manager', 'manager', 'manager', 'manager', ...Array(31).fill('agent')]; + const uniqueNames = generateUniqueNames(36); + + // Distribuir usuários entre os times + for (let i = 0; i < 36; i++) { + const userId = crypto.randomUUID(); + const role = roles[i]; + const teamId = teams[i % 5]; + const name = uniqueNames[i]; + const email = `${name.split(' ')[0].toLowerCase()}.${name.split(' ')[1].toLowerCase()}.${i}@teste.com`; + + await pool.query( + `INSERT INTO users (id, tenant_id, team_id, name, email, password_hash, role, status) VALUES (?, ?, ?, ?, ?, ?, ?, 'active')`, + [userId, tenantId, teamId, name, email, 'dummy_hash_not_for_login', role] + ); + users.push(userId); + } + console.log('✅ 36 usuários criados (5 managers, 31 agents).'); + + // 5. Gerar attendances (01/01/2026 até 22/04/2026) + // 112 dias totais + const startDate = new Date('2026-01-01T08:00:00Z'); + const endDate = new Date('2026-04-22T18:00:00Z'); + + let currentDay = new Date(startDate); + const attendancesToInsert = []; + + console.log('⏳ Gerando dados de attendances em memória...'); + while (currentDay <= endDate) { + // Pular domingos para dar mais realismo, ou deixar todos os dias? Vamos deixar todos os dias. + for (const userId of users) { + const numAttendances = getRandomInt(3, 5); // 3 a 5 por dia por usuário + for (let a = 0; a < numAttendances; a++) { + const createdAt = new Date(currentDay); + createdAt.setHours(getRandomInt(8, 17), getRandomInt(0, 59), getRandomInt(0, 59)); + + const isConverted = Math.random() > 0.7; // 30% conversão + const reqProduct = getRandomItem(PRODUCTS); + const soldProduct = isConverted ? reqProduct : null; + + let score = getRandomInt(40, 100); + if (isConverted) score = getRandomInt(85, 100); + + attendancesToInsert.push([ + crypto.randomUUID(), + tenantId, + userId, + isConverted ? "Venda efetuada com sucesso" : "Cliente não finalizou a compra", + isConverted ? "Cliente comprou com sucesso. Excelente atendimento." : "Cliente achou o valor alto e desistiu.", + score, + getRandomInt(1, 45), // first_response_time_min + getRandomInt(10, 120), // handling_time_min + isConverted ? 'Ganhos' : (Math.random() > 0.5 ? 'Perdidos' : getRandomItem(funnels)), // funnel_stage + getRandomItem(origins), // origin + reqProduct, + soldProduct, + isConverted ? 1 : 0, + JSON.stringify(isConverted ? [] : ["Faltou oferecer desconto", "Demora no primeiro contato"]), + JSON.stringify(["Melhorar rapport inicial"]), + createdAt + ]); + } + } + currentDay.setDate(currentDay.getDate() + 1); + } + + // 6. Inserir em lotes + console.log(`⏳ Inserindo ${attendancesToInsert.length} attendances no banco em lotes...`); + const batchSize = 1000; + for (let i = 0; i < attendancesToInsert.length; i += batchSize) { + const batch = attendancesToInsert.slice(i, i + batchSize); + 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, created_at) + VALUES ?`, + [batch] + ); + process.stdout.write(`\r✅ Inseridos ${Math.min(i + batchSize, attendancesToInsert.length)} / ${attendancesToInsert.length}`); + } + console.log('\n🎉 Todos os dados foram gerados com sucesso!'); + process.exit(0); + } catch (error) { + console.error('❌ Erro:', error); + process.exit(1); + } +} + +run(); diff --git a/debug.txt b/debug.txt deleted file mode 100644 index 8cf10a2..0000000 --- a/debug.txt +++ /dev/null @@ -1,27 +0,0 @@ -Look at `playNotificationSound`: -```javascript - const playNotificationSound = () => { - if (currentUser?.sound_enabled !== false && audioRef.current) { - // Reset time to 0 to allow rapid replays - audioRef.current.currentTime = 0; - const playPromise = audioRef.current.play(); -``` -Is `currentUser` loaded when `loadNotifications` fires for the first time after `isInitialLoadRef` is false? -Yes, `useEffect` calls `fetchCurrentUser()`, which sets `currentUser`. - -Wait. `setInterval` uses a closure over the state! -```javascript - useEffect(() => { - fetchCurrentUser(); - loadNotifications(); - const interval = setInterval(loadNotifications, 10000); - return () => clearInterval(interval); - }, [navigate]); -``` -Oh my god. The `setInterval` callback `loadNotifications` captures the *initial* state variables, including `currentUser`, which is `null` on the first render! -If `currentUser` is `null` inside the closure, `currentUser?.sound_enabled !== false` evaluates to `true !== false` which is `true`. So that's not blocking it. -BUT `audioRef.current` might not have been rendered yet? No, `audioRef` is a ref, so it mutates in place. The closure always sees the latest `audioRef.current`. - -So why does it fail or not play? -Is the browser policy blocking it silently without logging? -Let's add a robust, standalone Audio approach that doesn't rely on the DOM tag if it fails, or maybe just force a click handler to "unlock" the audio context. diff --git a/debug2.txt b/debug2.txt deleted file mode 100644 index 9c14958..0000000 --- a/debug2.txt +++ /dev/null @@ -1,36 +0,0 @@ -Let's see what happens during returnToSuperAdmin: -1. Decode superAdminToken -2. localStorage.setItem('ctms_token', superAdminToken) -3. localStorage.setItem('ctms_user_id', payload.id) <-- u_71657ec7 -4. localStorage.setItem('ctms_tenant_id', payload.tenant_id || 'system') <-- 'system' -5. window.location.reload() - -Then the app reloads. -1. AuthGuard mounts -2. const storedUserId = localStorage.getItem('ctms_user_id') <-- u_71657ec7 -3. const fetchedUser = await getUserById(storedUserId) - -> GET /api/users/u_71657ec7 - -Backend handles GET /api/users/u_71657ec7: -req.user is derived from jwt.verify(superAdminToken) -So req.user.role === 'super_admin' -req.user.tenant_id === 'system' (or null) - -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) { ... } -res.json(rows[0]); - -Wait. If this is a 500 error, why does it crash? -What if `req.user` is undefined? But `authenticateToken` middleware ensures `req.user` exists. -Maybe `pool.query` fails? -No, wait. You said: -"now its sending me to login page, congrats" -Then I fixed the Base64 pad. -Then you said: -"i am still having this problem, when i go to impersionate the pages blink and i go to the tenant pages, good, but when i press to go back and stop impersionate it redirects me to the goddam login page" -Then you said: -"ok, its working but its giving some errors in dev tools and this errors is showing our users id and path ... XHRGET https://fasto.blyzer.com.br/api/users/u_71657ec7 [HTTP/2 500 14ms]" - -Why 500? -Let's add a console.log in the catch block of `/users/:idOrSlug` to see the actual error. diff --git a/debug3.txt b/debug3.txt deleted file mode 100644 index 00dd556..0000000 --- a/debug3.txt +++ /dev/null @@ -1,20 +0,0 @@ -Wait, let's look at `index.js` line 354 BEFORE my fix: -```javascript - 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) { -``` -The ONLY way this throws 500 is if: -1. `pool.query` fails (e.g. database disconnected, which isn't the case). -2. `req.user` is somehow null or undefined (but `authenticateToken` guarantees it exists). -3. `rows[0]` is undefined (but `rows.length === 0` handles that). - -Wait, what if `req.user.tenant_id` is null? `null !== 'tenant_xyz'` is true, so it returns 403, not 500. - -What if the 500 is coming from `GET /api/users/u_71657ec7` but it's not actually hitting `/users/:idOrSlug`? -Is there a middleware or something? No. - -Ah! What if the user you are impersonating was deleted from the database? `rows.length === 0` -> returns 404, not 500. - -Let's check the local logs AGAIN after my recent rebuild. I added `console.error('Error in GET /users/:idOrSlug:', error);` -Let's deliberately trigger the error locally to see it. But I don't have the browser. diff --git a/docker-compose.yml b/docker-compose.yml index eb48de5..aa5cc63 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -57,7 +57,7 @@ services: MAX_BACKUPS: 3 # Mantém apenas os 3 últimos dias INIT_BACKUP: "1" # Faz um backup imediatamente ao ligar o container volumes: - - /opt/backups_db/fasto:/backup + - /opt/backups_db/fastogemi:/backup networks: - fasto-net diff --git a/fix_db.js b/fix_db.js deleted file mode 100644 index e393b97..0000000 --- a/fix_db.js +++ /dev/null @@ -1,10 +0,0 @@ -const mysql = require('mysql2/promise'); -async function run() { - const pool = mysql.createPool({ host: '127.0.0.1', user: 'root', password: 'secret_pass', database: 'fasto_db', port: 3306 }); - try { - await pool.query("ALTER TABLE attendances MODIFY COLUMN funnel_stage VARCHAR(255) NOT NULL DEFAULT 'Novo'"); - console.log("Success"); - } catch(e) { console.error(e); } - pool.end(); -} -run(); diff --git a/index.html b/index.html index c871628..ec0e148 100644 --- a/index.html +++ b/index.html @@ -46,6 +46,6 @@
- +