diff --git a/backend/server.js b/backend/server.js index 8c123fc..5ed9eea 100644 --- a/backend/server.js +++ b/backend/server.js @@ -22,94 +22,6 @@ const pool = new Pool({ pool.on('connect', () => console.log('[PG] Connected to PostgreSQL')); pool.on('error', (err) => console.error('[PG] Pool error:', err.message)); -// ============================================================================= -// PostgreSQL Translator & Compatibility Wrapper -// ============================================================================= -function translateQuery(query, params) { - if (typeof query !== 'string') return { sql: query, pgParams: params }; - let sql = query; - let pgParams = params ? (Array.isArray(params) ? [...params] : [params]) : []; - - sql = sql.replace(/`([^`]+)`/g, '"$1"'); - - if (sql.match(/INSERT\s+INTO\s+([^\s]+)\s+SET\s+\?/i)) { - const table = sql.match(/INSERT\s+INTO\s+([^\s]+)\s+SET\s+\?/i)[1].replace(/"/g, ''); - const dataObj = pgParams[0]; - if (dataObj && typeof dataObj === 'object') { - const keys = Object.keys(dataObj); - const cols = keys.map(k => `"${k}"`).join(', '); - const valuesPlaceholder = keys.map((_, idx) => `$${idx + 1}`).join(', '); - sql = `INSERT INTO "${table}" (${cols}) VALUES (${valuesPlaceholder})`; - pgParams = keys.map(k => dataObj[k]); - } - } - - if (sql.match(/UPDATE\s+([^\s]+)\s+SET\s+\?\s+WHERE\s+(.+)/i)) { - const match = sql.match(/UPDATE\s+([^\s]+)\s+SET\s+\?\s+WHERE\s+(.+)/i); - const table = match[1].replace(/"/g, ''); - const whereClause = match[2]; - const dataObj = pgParams[0]; - const extraParams = pgParams.slice(1); - if (dataObj && typeof dataObj === 'object') { - const keys = Object.keys(dataObj); - let paramIdx = 1; - const setClause = keys.map(k => `"${k}" = $${paramIdx++}`).join(', '); - let postgresWhere = whereClause; - let tempIdx = paramIdx; - postgresWhere = postgresWhere.replace(/\?/g, () => `$${tempIdx++}`); - sql = `UPDATE "${table}" SET ${setClause} WHERE ${postgresWhere}`; - pgParams = [...keys.map(k => dataObj[k]), ...extraParams]; - } - } - - let count = 1; - sql = sql.replace(/\?/g, () => `$${count++}`); - - if (sql.match(/ON\s+DUPLICATE\s+KEY\s+UPDATE/i)) { - if (sql.toLowerCase().includes('settings')) sql = sql.replace(/ON\s+DUPLICATE\s+KEY\s+UPDATE\s+data\s*=\s*VALUES\(data\)|ON\s+DUPLICATE\s+KEY\s+UPDATE\s+data\s*=\s*\$4|ON\s+DUPLICATE\s+KEY\s+UPDATE\s+data\s*=\s*EXCLUDED\.data/i, 'ON CONFLICT (id) DO UPDATE SET data = EXCLUDED.data'); - else if (sql.toLowerCase().includes('google_tokens')) sql = sql.replace(/ON\s+DUPLICATE\s+KEY\s+UPDATE[\s\S]+/i, 'ON CONFLICT (owner_id) DO UPDATE SET refresh_token = EXCLUDED.refresh_token, access_token = EXCLUDED.access_token, expiry_date = EXCLUDED.expiry_date'); - else if (sql.toLowerCase().includes('usuarios')) sql = sql.replace(/ON\s+DUPLICATE\s+KEY\s+UPDATE[\s\S]+/i, 'ON CONFLICT (id) DO UPDATE SET nome = EXCLUDED.nome, cro = EXCLUDED.cro, cro_uf = EXCLUDED.cro_uf, celular = EXCLUDED.celular, especialidade = EXCLUDED.especialidade'); - else if (sql.toLowerCase().includes('vinculos')) sql = sql.replace(/ON\s+DUPLICATE\s+KEY\s+UPDATE[\s\S]+/i, 'ON CONFLICT (usuario_id, clinica_id) DO UPDATE SET role = EXCLUDED.role'); - else if (sql.toLowerCase().includes('dentistas_horarios')) sql = sql.replace(/ON\s+DUPLICATE\s+KEY\s+UPDATE[\s\S]+/i, 'ON CONFLICT (id) DO UPDATE SET dia_semana = EXCLUDED.dia_semana, hora_inicio = EXCLUDED.hora_inicio, hora_fim = EXCLUDED.hora_fim, ativo = EXCLUDED.ativo'); - else if (sql.toLowerCase().includes('dentistas')) sql = sql.replace(/ON\s+DUPLICATE\s+KEY\s+UPDATE[\s\S]+/i, 'ON CONFLICT (id) DO UPDATE SET nome = EXCLUDED.nome, especialidade = EXCLUDED.especialidade'); - else if (sql.toLowerCase().includes('procedimentos_valores_planos')) sql = sql.replace(/ON\s+DUPLICATE\s+KEY\s+UPDATE[\s\S]+/i, 'ON CONFLICT (id) DO UPDATE SET valor = EXCLUDED.valor'); - else if (sql.toLowerCase().includes('procedimentos')) sql = sql.replace(/ON\s+DUPLICATE\s+KEY\s+UPDATE[\s\S]+/i, 'ON CONFLICT (id) DO UPDATE SET nome = EXCLUDED.nome, "especialidadeId" = EXCLUDED."especialidadeId", "especialidadeNome" = EXCLUDED."especialidadeNome", "valorParticular" = EXCLUDED."valorParticular", "codigoInterno" = EXCLUDED."codigoInterno", categoria = EXCLUDED.categoria, tipo_regiao = EXCLUDED.tipo_regiao, exige_face = EXCLUDED.exige_face, codigo = EXCLUDED.codigo'); - } - - sql = sql.replace(/DATE_FORMAT\(([^,]+),\s*['"]%Y-%m['"]\)/gi, "TO_CHAR($1, 'YYYY-MM')"); - sql = sql.replace(/DATE_SUB\(CURDATE\(\),\s*INTERVAL\s+(\d+)\s+MONTH\)/gi, "CURRENT_DATE - INTERVAL '$1 MONTH'"); - if (sql.match(/SET\s+FOREIGN_KEY_CHECKS\s*=\s*\d+/i)) sql = "SELECT 1"; - - return { sql, pgParams }; -} - -const originalPoolQuery = pool.query.bind(pool); -pool.query = async function (queryText, params) { - const { sql, pgParams } = translateQuery(queryText, params); - const result = await originalPoolQuery(sql, pgParams); - const resArray = [result.rows, result.fields]; - resArray.rows = result.rows; - resArray.rowCount = result.rowCount; - return resArray; -}; - -pool.getConnection = async function () { - const client = await pool.connect(); - const originalClientQuery = client.query.bind(client); - client.query = async function (queryText, params) { - const { sql, pgParams } = translateQuery(queryText, params); - const result = await originalClientQuery(sql, pgParams); - const resArray = [result.rows, result.fields]; - resArray.rows = result.rows; - resArray.rowCount = result.rowCount; - return resArray; - }; - client.beginTransaction = async () => await originalClientQuery('BEGIN'); - client.commit = async () => await originalClientQuery('COMMIT'); - client.rollback = async () => await originalClientQuery('ROLLBACK'); - return client; -}; - // Helper: transaction wrapper async function withTransaction(fn) { const client = await pool.connect(); @@ -312,7 +224,7 @@ app.get('/api/oauth2callback', async (req, res) => { app.get('/api/auth/google/status', async (req, res) => { try { - const [rows] = await pool.query('SELECT owner_id, updated_at FROM google_tokens'); + const { rows } = await pool.query('SELECT owner_id, updated_at FROM google_tokens'); res.json(rows); } catch (err) { res.status(500).json({ error: err.message }); } }); @@ -358,33 +270,32 @@ app.post('/api/dentistas/magic-link', async (req, res) => { app.post('/api/dentistas/register', async (req, res) => { const { id, nome, email, senha, cro, cro_uf, celular, especialidade, clinicaId } = req.body; try { - await pool.query('SET FOREIGN_KEY_CHECKS = 0'); - // 1. Create User - await pool.query(` - INSERT INTO usuarios (id, nome, email, senha, cro, cro_uf, celular, especialidade) - VALUES (?, ?, ?, ?, ?, ?, ?, ?) - ON DUPLICATE KEY UPDATE - nome = VALUES(nome), cro = VALUES(cro), cro_uf = VALUES(cro_uf), - celular = VALUES(celular), especialidade = VALUES(especialidade) - `, [id, nome, email, senha, cro, cro_uf, celular, especialidade]); + await withTransaction(async (client) => { + // 1. Create or update User + await client.query(` + INSERT INTO usuarios (id, nome, email, senha, cro, cro_uf, celular, especialidade) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8) + ON CONFLICT (id) DO UPDATE SET + nome = EXCLUDED.nome, cro = EXCLUDED.cro, cro_uf = EXCLUDED.cro_uf, + celular = EXCLUDED.celular, especialidade = EXCLUDED.especialidade + `, [id, nome, email, senha, cro, cro_uf, celular, especialidade]); - // 2. Create Link to clinic - const vinculoId = `v_${id}_${clinicaId}`; - await pool.query(` - INSERT INTO vinculos (id, usuario_id, clinica_id, role) - VALUES (?, ?, ?, 'dentista') - ON DUPLICATE KEY UPDATE role = 'dentista' - `, [vinculoId, id, clinicaId]); + // 2. Create or update link to clinic + const vinculoId = `v_${id}_${clinicaId}`; + await client.query(` + INSERT INTO vinculos (id, usuario_id, clinica_id, role) + VALUES ($1, $2, $3, 'dentista') + ON CONFLICT (usuario_id, clinica_id) DO UPDATE SET role = 'dentista' + `, [vinculoId, id, clinicaId]); - // 3. Create Dentist record for agenda - await pool.query(` - INSERT INTO dentistas (id, clinica_id, nome, email, telefone, especialidade, corAgenda) - VALUES (?, ?, ?, ?, ?, ?, ?) - ON DUPLICATE KEY UPDATE - nome = VALUES(nome), especialidade = VALUES(especialidade) - `, [id, clinicaId, nome, email, celular, especialidade, '#2563eb']); - - await pool.query('SET FOREIGN_KEY_CHECKS = 1'); + // 3. Create or update Dentist record for agenda + await client.query(` + INSERT INTO dentistas (id, clinica_id, nome, email, telefone, especialidade, "corAgenda") + VALUES ($1, $2, $3, $4, $5, $6, $7) + ON CONFLICT (id) DO UPDATE SET + nome = EXCLUDED.nome, especialidade = EXCLUDED.especialidade + `, [id, clinicaId, nome, email, celular, especialidade, '#2563eb']); + }); res.json({ success: true }); } catch (err) { res.status(500).json({ error: err.message }); } }); @@ -392,7 +303,7 @@ app.post('/api/dentistas/register', async (req, res) => { app.get('/api/google/events', async (req, res) => { try { // 1. Check for connected accounts in google_tokens - const [tokenRows] = await pool.query('SELECT * FROM google_tokens'); + const { rows: tokenRows } = await pool.query('SELECT * FROM google_tokens'); const allEvents = []; const now = new Date(); const timeMin = now.toISOString(); @@ -418,7 +329,7 @@ app.get('/api/google/events', async (req, res) => { if (response.data.items) { // Try to resolve owner name let ownerName = tokenData.owner_id; - const [dent] = await pool.query('SELECT nome FROM dentistas WHERE id = ?', [tokenData.owner_id]); + const { rows: dent } = await pool.query('SELECT nome FROM dentistas WHERE id = $1', [tokenData.owner_id]); if (dent[0]) ownerName = dent[0].nome; const mapped = response.data.items.map(item => ({ @@ -439,9 +350,9 @@ app.get('/api/google/events', async (req, res) => { } // 2. Legacy Support: Check Settings for Public Calendars (API KEY) - const [rows] = await pool.query("SELECT data FROM settings WHERE id = 'main'"); - if (rows[0]) { - const settings = JSON.parse(rows[0].data); + const { rows: settingsRows } = await pool.query("SELECT data FROM settings WHERE id = 'main'"); + if (settingsRows[0]) { + const settings = JSON.parse(settingsRows[0].data); const apiKey = settings.googleApiKey; if (apiKey) { const legacyIds = []; @@ -450,7 +361,7 @@ app.get('/api/google/events', async (req, res) => { if (settings.googleCalendarIds) { for (const [dentistId, calId] of Object.entries(settings.googleCalendarIds)) { if (calId && !tokenRows.find(t => t.owner_id === dentistId)) { - const [dName] = await pool.query('SELECT nome FROM dentistas WHERE id = ?', [dentistId]); + const { rows: dName } = await pool.query('SELECT nome FROM dentistas WHERE id = $1', [dentistId]); legacyIds.push({ id: calId, owner: dName[0]?.nome || 'DENTISTA' }); } } @@ -503,7 +414,7 @@ app.get('/api/dashboard/stats', async (req, res) => { // Growth data (last 6 months) const { rows: growth } = await pool.query(` - SELECT + SELECT TO_CHAR(datavencimento, 'YYYY-MM') as mes, SUM(CASE WHEN tipo = 'RECEITA' OR tipo IS NULL THEN valor ELSE 0 END) as receita, SUM(CASE WHEN tipo = 'DESPESA' THEN valor ELSE 0 END) as despesa @@ -576,7 +487,7 @@ app.put('/api/notificacoes/:id/read', async (req, res) => { // --- PACIENTES --- app.get('/api/pacientes', async (req, res) => { try { - const [rows] = await pool.query('SELECT * FROM pacientes'); + const { rows } = await pool.query('SELECT * FROM pacientes'); res.json(rows); } catch (err) { res.status(500).json({ error: err.message }); @@ -586,7 +497,8 @@ app.get('/api/pacientes', async (req, res) => { app.post('/api/pacientes', async (req, res) => { try { const p = req.body; - await pool.query('INSERT INTO pacientes SET ?', p); + const ins = buildInsert('pacientes', p); + await pool.query(ins.text, ins.values); res.json({ success: true, data: p }); } catch (err) { res.status(500).json({ error: err.message }); @@ -595,7 +507,8 @@ app.post('/api/pacientes', async (req, res) => { app.put('/api/pacientes/:id', async (req, res) => { try { - await pool.query('UPDATE pacientes SET ? WHERE id = ?', [req.body, req.params.id]); + const upd = buildUpdate('pacientes', req.body, 'id', req.params.id); + await pool.query(upd.text, upd.values); res.json({ success: true }); } catch (err) { res.status(500).json({ error: err.message }); @@ -609,11 +522,11 @@ app.get('/api/dentistas', async (req, res) => { let query = 'SELECT * FROM dentistas'; const params = []; if (clinicaId) { - query += ' WHERE clinica_id = ?'; + query += ' WHERE clinica_id = $1'; params.push(clinicaId); } query += ' ORDER BY ordem ASC, nome ASC'; - const [rows] = await pool.query(query, params); + const { rows } = await pool.query(query, params); res.json(rows.map(d => ({ ...d, ativo: !!d.ativo }))); } catch (err) { res.status(500).json({ error: err.message }); } }); @@ -622,62 +535,53 @@ app.post('/api/dentistas', async (req, res) => { try { const d = req.body; // Automatically set order to the end - const [counts] = await pool.query('SELECT COUNT(*) as count FROM dentistas'); - const ordem = counts[0].count; - await pool.query('INSERT INTO dentistas SET ?', { ...d, ordem }); + const { rows: counts } = await pool.query('SELECT COUNT(*) as count FROM dentistas'); + const ordem = parseInt(counts[0].count); + const ins = buildInsert('dentistas', { ...d, ordem }); + await pool.query(ins.text, ins.values); res.json({ success: true }); } catch (err) { res.status(500).json({ error: err.message }); } }); app.put('/api/dentistas/reorder', async (req, res) => { const { orders } = req.body; // Array of {id, ordem} - const connection = await pool.getConnection(); try { - await connection.beginTransaction(); - for (const item of orders) { - await connection.query('UPDATE dentistas SET ordem = ? WHERE id = ?', [item.ordem, item.id]); - } - await connection.commit(); + await withTransaction(async (client) => { + for (const item of orders) { + await client.query('UPDATE dentistas SET ordem = $1 WHERE id = $2', [item.ordem, item.id]); + } + }); res.json({ success: true }); } catch (err) { - await connection.rollback(); res.status(500).json({ error: err.message }); - } finally { connection.release(); } + } }); app.put('/api/dentistas/:id', async (req, res) => { try { - await pool.query('UPDATE dentistas SET ? WHERE id = ?', [req.body, req.params.id]); + const upd = buildUpdate('dentistas', req.body, 'id', req.params.id); + await pool.query(upd.text, upd.values); res.json({ success: true }); } catch (err) { res.status(500).json({ error: err.message }); } }); app.delete('/api/dentistas/:id', async (req, res) => { - const connection = await pool.getConnection(); try { const { id } = req.params; - await connection.beginTransaction(); - - // 1. Remove Google Tokens associated with this dentist - await connection.query('DELETE FROM google_tokens WHERE owner_id = ?', [id]); - - // 2. Remove clinical links (if any) - await connection.query('DELETE FROM vinculos WHERE usuario_id = ?', [id]); - - // 3. Set to NULL in agendamentos if it exists - await connection.query('UPDATE agendamentos SET dentistaId = NULL WHERE dentistaId = ?', [id]); - - // 4. Delete from dentistas table - await connection.query('DELETE FROM dentistas WHERE id = ?', [id]); - - await connection.commit(); + await withTransaction(async (client) => { + // 1. Remove Google Tokens associated with this dentist + await client.query('DELETE FROM google_tokens WHERE owner_id = $1', [id]); + // 2. Remove clinical links (if any) + await client.query('DELETE FROM vinculos WHERE usuario_id = $1', [id]); + // 3. Set to NULL in agendamentos if it exists + await client.query('UPDATE agendamentos SET dentistaid = NULL WHERE dentistaid = $1', [id]); + // 4. Delete from dentistas table + await client.query('DELETE FROM dentistas WHERE id = $1', [id]); + }); res.json({ success: true }); } catch (err) { - await connection.rollback(); console.error('Error deleting dentist:', err); res.status(500).json({ error: err.message }); - } finally { - connection.release(); } }); @@ -686,14 +590,14 @@ app.get('/api/dentistas/:dentistaId/horarios', async (req, res) => { try { const { dentistaId } = req.params; const { clinicaId } = req.query; - let query = 'SELECT * FROM dentistas_horarios WHERE dentista_id = ?'; + let query = 'SELECT * FROM dentistas_horarios WHERE dentista_id = $1'; const params = [dentistaId]; if (clinicaId) { - query += ' AND clinica_id = ?'; + query += ' AND clinica_id = $2'; params.push(clinicaId); } query += ' ORDER BY dia_semana ASC, hora_inicio ASC'; - const [rows] = await pool.query(query, params); + const { rows } = await pool.query(query, params); res.json(rows); } catch (err) { res.status(500).json({ error: err.message }); } }); @@ -701,20 +605,21 @@ app.get('/api/dentistas/:dentistaId/horarios', async (req, res) => { app.post('/api/dentistas/horarios', async (req, res) => { try { const { id, dentista_id, clinica_id, dia_semana, hora_inicio, hora_fim, ativo } = req.body; + const horaId = id || `h_${Math.random().toString(36).substring(2, 9)}`; await pool.query(` INSERT INTO dentistas_horarios (id, dentista_id, clinica_id, dia_semana, hora_inicio, hora_fim, ativo) - VALUES (?, ?, ?, ?, ?, ?, ?) - ON DUPLICATE KEY UPDATE - dia_semana = VALUES(dia_semana), hora_inicio = VALUES(hora_inicio), - hora_fim = VALUES(hora_fim), ativo = VALUES(ativo) - `, [id || `h_${Math.random().toString(36).substring(2, 9)}`, dentista_id, clinica_id, dia_semana, hora_inicio, hora_fim, ativo ?? 1]); + VALUES ($1, $2, $3, $4, $5, $6, $7) + ON CONFLICT (id) DO UPDATE SET + dia_semana = EXCLUDED.dia_semana, hora_inicio = EXCLUDED.hora_inicio, + hora_fim = EXCLUDED.hora_fim, ativo = EXCLUDED.ativo + `, [horaId, dentista_id, clinica_id, dia_semana, hora_inicio, hora_fim, ativo ?? 1]); res.json({ success: true }); } catch (err) { res.status(500).json({ error: err.message }); } }); app.delete('/api/dentistas/horarios/:id', async (req, res) => { try { - await pool.query('DELETE FROM dentistas_horarios WHERE id = ?', [req.params.id]); + await pool.query('DELETE FROM dentistas_horarios WHERE id = $1', [req.params.id]); res.json({ success: true }); } catch (err) { res.status(500).json({ error: err.message }); } }); @@ -727,17 +632,17 @@ app.get('/api/reports/productivity', async (req, res) => { // Financial Totals const { rows: finRows } = await pool.query(` - SELECT + SELECT SUM(CASE WHEN tipo = 'RECEITA' AND status = 'Pago' THEN valor ELSE 0 END) as receita, SUM(CASE WHEN tipo = 'DESPESA' AND status = 'Pago' THEN valor ELSE 0 END) as despesa, COUNT(*) as totalTransacoes - FROM financeiro + FROM financeiro WHERE clinica_id = $1 AND datavencimento BETWEEN $2 AND $3 `, [clinicaId, start || '2000-01-01', end || '2100-01-01']); // Appointments by Dentist const { rows: agRows } = await pool.query(` - SELECT + SELECT d.nome as dentista, COUNT(a.id) as totalAgendamentos, SUM(CASE WHEN a.status = 'Concluido' THEN 1 ELSE 0 END) as concluidos, @@ -773,8 +678,8 @@ app.post('/api/agendamentos', async (req, res) => { // Lock check: SELECT the conflicting row FOR UPDATE to prevent race conditions if (dentistaId && start) { const { rows: conflict } = await client.query( - `SELECT id FROM agendamentos - WHERE dentistaid = $1 AND start_time = $2 + `SELECT id FROM agendamentos + WHERE dentistaid = $1 AND start_time = $2 LIMIT 1 FOR UPDATE`, [dentistaId, start] ); @@ -784,7 +689,7 @@ app.post('/api/agendamentos', async (req, res) => { } const id = rest.id || `ag_${Date.now()}_${Math.random().toString(36).substring(2, 7)}`; - + // Map keys from camelCase/old attributes to correct lowercase PG columns if necessary const payload = { id, @@ -849,7 +754,7 @@ app.put('/api/agendamentos/:id', async (req, res) => { app.delete('/api/agendamentos/:id', async (req, res) => { try { - await pool.query('DELETE FROM agendamentos WHERE id = ?', [req.params.id]); + await pool.query('DELETE FROM agendamentos WHERE id = $1', [req.params.id]); res.json({ success: true }); } catch (err) { res.status(500).json({ error: err.message }); } }); @@ -857,7 +762,7 @@ app.delete('/api/agendamentos/:id', async (req, res) => { // --- ESPECIALIDADES --- app.get('/api/especialidades', async (req, res) => { try { - const [rows] = await pool.query('SELECT * FROM especialidades ORDER BY ordem ASC, nome ASC'); + const { rows } = await pool.query('SELECT * FROM especialidades ORDER BY ordem ASC, nome ASC'); res.json(rows); } catch (err) { res.status(500).json({ error: err.message }); } }); @@ -865,39 +770,39 @@ app.get('/api/especialidades', async (req, res) => { app.post('/api/especialidades', async (req, res) => { try { // Automatically set order to the end - const [counts] = await pool.query('SELECT COUNT(*) as count FROM especialidades'); - const ordem = counts[0].count; - await pool.query('INSERT INTO especialidades SET ?', { ...req.body, ordem }); + const { rows: counts } = await pool.query('SELECT COUNT(*) as count FROM especialidades'); + const ordem = parseInt(counts[0].count); + const ins = buildInsert('especialidades', { ...req.body, ordem }); + await pool.query(ins.text, ins.values); res.json({ success: true }); } catch (err) { res.status(500).json({ error: err.message }); } }); app.put('/api/especialidades/reorder', async (req, res) => { const { orders } = req.body; // Array of {id, ordem} - const connection = await pool.getConnection(); try { - await connection.beginTransaction(); - for (const item of orders) { - await connection.query('UPDATE especialidades SET ordem = ? WHERE id = ?', [item.ordem, item.id]); - } - await connection.commit(); + await withTransaction(async (client) => { + for (const item of orders) { + await client.query('UPDATE especialidades SET ordem = $1 WHERE id = $2', [item.ordem, item.id]); + } + }); res.json({ success: true }); } catch (err) { - await connection.rollback(); res.status(500).json({ error: err.message }); - } finally { connection.release(); } + } }); app.put('/api/especialidades/:id', async (req, res) => { try { - await pool.query('UPDATE especialidades SET ? WHERE id = ?', [req.body, req.params.id]); + const upd = buildUpdate('especialidades', req.body, 'id', req.params.id); + await pool.query(upd.text, upd.values); res.json({ success: true }); } catch (err) { res.status(500).json({ error: err.message }); } }); app.delete('/api/especialidades/:id', async (req, res) => { try { - await pool.query('DELETE FROM especialidades WHERE id = ?', [req.params.id]); + await pool.query('DELETE FROM especialidades WHERE id = $1', [req.params.id]); res.json({ success: true }); } catch (err) { res.status(500).json({ error: err.message }); } }); @@ -905,28 +810,30 @@ app.delete('/api/especialidades/:id', async (req, res) => { // --- PLANOS --- app.get('/api/planos', async (req, res) => { try { - const [rows] = await pool.query('SELECT * FROM planos'); + const { rows } = await pool.query('SELECT * FROM planos'); res.json(rows); } catch (err) { res.status(500).json({ error: err.message }); } }); app.post('/api/planos', async (req, res) => { try { - await pool.query('INSERT INTO planos SET ?', req.body); + const ins = buildInsert('planos', req.body); + await pool.query(ins.text, ins.values); res.json({ success: true }); } catch (err) { res.status(500).json({ error: err.message }); } }); app.put('/api/planos/:id', async (req, res) => { try { - await pool.query('UPDATE planos SET ? WHERE id = ?', [req.body, req.params.id]); + const upd = buildUpdate('planos', req.body, 'id', req.params.id); + await pool.query(upd.text, upd.values); res.json({ success: true }); } catch (err) { res.status(500).json({ error: err.message }); } }); app.delete('/api/planos/:id', async (req, res) => { try { - await pool.query('DELETE FROM planos WHERE id = ?', [req.params.id]); + await pool.query('DELETE FROM planos WHERE id = $1', [req.params.id]); res.json({ success: true }); } catch (err) { res.status(500).json({ error: err.message }); } }); @@ -934,8 +841,8 @@ app.delete('/api/planos/:id', async (req, res) => { // --- PROCEDIMENTOS --- app.get('/api/procedimentos', async (req, res) => { try { - const [procs] = await pool.query('SELECT * FROM procedimentos ORDER BY ordem ASC, nome ASC'); - const [valores] = await pool.query('SELECT * FROM procedimentos_valores_planos'); + const { rows: procs } = await pool.query('SELECT * FROM procedimentos ORDER BY ordem ASC, nome ASC'); + const { rows: valores } = await pool.query('SELECT * FROM procedimentos_valores_planos'); const data = procs.map(p => ({ ...p, @@ -949,21 +856,19 @@ app.get('/api/procedimentos', async (req, res) => { }); app.post('/api/procedimentos', async (req, res) => { - const connection = await pool.getConnection(); try { - await connection.beginTransaction(); const { valoresPlanos, ...proc } = req.body; // Auto-generate codigoInterno for PARTICULAR let codigoInternoVal = proc.codigoInterno; if (proc.categoria === 'Particular' && (!codigoInternoVal || codigoInternoVal === '')) { - const [rows] = await connection.query('SELECT COUNT(*) as count FROM procedimentos WHERE categoria = "Particular"'); - codigoInternoVal = (rows[0].count + 1).toString(); + const { rows: partCount } = await pool.query("SELECT COUNT(*) as count FROM procedimentos WHERE categoria = 'Particular'"); + codigoInternoVal = (parseInt(partCount[0].count) + 1).toString(); } // Automatically set order to the end - const [counts] = await connection.query('SELECT COUNT(*) as count FROM procedimentos'); - const ordemValue = counts[0].count; + const { rows: counts } = await pool.query('SELECT COUNT(*) as count FROM procedimentos'); + const ordemValue = parseInt(counts[0].count); const payload = { ...proc, @@ -972,68 +877,63 @@ app.post('/api/procedimentos', async (req, res) => { tipo_regiao: proc.tipo_regiao || 'GERAL', exige_face: proc.exige_face ? 1 : 0 }; - if (!payload.id) payload.id = Math.random().toString(36).substring(2, 10).toUpperCase(); - await connection.query('INSERT INTO procedimentos SET ?', payload); - if (valoresPlanos && valoresPlanos.length > 0) { - for (const v of valoresPlanos) { - await connection.query('INSERT INTO procedimentos_valores_planos SET ?', { ...v, procedimentoId: payload.id }); + await withTransaction(async (client) => { + const ins = buildInsert('procedimentos', payload); + await client.query(ins.text, ins.values); + if (valoresPlanos && valoresPlanos.length > 0) { + for (const v of valoresPlanos) { + const vIns = buildInsert('procedimentos_valores_planos', { ...v, procedimentoId: payload.id }); + await client.query(vIns.text, vIns.values); + } } - } - await connection.commit(); + }); res.json({ success: true }); } catch (err) { - await connection.rollback(); res.status(500).json({ error: err.message }); - } finally { connection.release(); } + } }); app.put('/api/procedimentos/:id', async (req, res) => { - const connection = await pool.getConnection(); try { - await connection.beginTransaction(); const { valoresPlanos, ...proc } = req.body; + const updateData = { ...proc, exige_face: proc.exige_face ? 1 : 0 }; - const updateData = { - ...proc, - exige_face: proc.exige_face ? 1 : 0 - }; - - await connection.query('UPDATE procedimentos SET ? WHERE id = ?', [updateData, req.params.id]); - await connection.query('DELETE FROM procedimentos_valores_planos WHERE procedimentoId = ?', [req.params.id]); - if (valoresPlanos && valoresPlanos.length > 0) { - for (const v of valoresPlanos) { - await connection.query('INSERT INTO procedimentos_valores_planos SET ?', { ...v, procedimentoId: req.params.id }); + await withTransaction(async (client) => { + const upd = buildUpdate('procedimentos', updateData, 'id', req.params.id); + await client.query(upd.text, upd.values); + await client.query('DELETE FROM procedimentos_valores_planos WHERE "procedimentoId" = $1', [req.params.id]); + if (valoresPlanos && valoresPlanos.length > 0) { + for (const v of valoresPlanos) { + const vIns = buildInsert('procedimentos_valores_planos', { ...v, procedimentoId: req.params.id }); + await client.query(vIns.text, vIns.values); + } } - } - await connection.commit(); + }); res.json({ success: true }); } catch (err) { - await connection.rollback(); res.status(500).json({ error: err.message }); - } finally { connection.release(); } + } }); app.put('/api/procedimentos/reorder', async (req, res) => { const { orders } = req.body; // Array of {id, ordem} - const connection = await pool.getConnection(); try { - await connection.beginTransaction(); - for (const item of orders) { - await connection.query('UPDATE procedimentos SET ordem = ? WHERE id = ?', [item.ordem, item.id]); - } - await connection.commit(); + await withTransaction(async (client) => { + for (const item of orders) { + await client.query('UPDATE procedimentos SET ordem = $1 WHERE id = $2', [item.ordem, item.id]); + } + }); res.json({ success: true }); } catch (err) { - await connection.rollback(); res.status(500).json({ error: err.message }); - } finally { connection.release(); } + } }); app.delete('/api/procedimentos/:id', async (req, res) => { try { - await pool.query('DELETE FROM procedimentos WHERE id = ?', [req.params.id]); + await pool.query('DELETE FROM procedimentos WHERE id = $1', [req.params.id]); res.json({ success: true }); } catch (err) { res.status(500).json({ error: err.message }); } }); @@ -1041,28 +941,30 @@ app.delete('/api/procedimentos/:id', async (req, res) => { // --- FINANCEIRO --- app.get('/api/financeiro', async (req, res) => { try { - const [rows] = await pool.query('SELECT * FROM financeiro'); + const { rows } = await pool.query('SELECT * FROM financeiro'); res.json(rows.map(r => ({ ...r, valor: parseFloat(r.valor || 0) }))); } catch (err) { res.status(500).json({ error: err.message }); } }); app.post('/api/financeiro', async (req, res) => { try { - await pool.query('INSERT INTO financeiro SET ?', req.body); + const ins = buildInsert('financeiro', req.body); + await pool.query(ins.text, ins.values); res.json({ success: true }); } catch (err) { res.status(500).json({ error: err.message }); } }); app.put('/api/financeiro/:id', async (req, res) => { try { - await pool.query('UPDATE financeiro SET ? WHERE id = ?', [req.body, req.params.id]); + const upd = buildUpdate('financeiro', req.body, 'id', req.params.id); + await pool.query(upd.text, upd.values); res.json({ success: true }); } catch (err) { res.status(500).json({ error: err.message }); } }); app.delete('/api/financeiro/:id', async (req, res) => { try { - await pool.query('DELETE FROM financeiro WHERE id = ?', [req.params.id]); + await pool.query('DELETE FROM financeiro WHERE id = $1', [req.params.id]); res.json({ success: true }); } catch (err) { res.status(500).json({ error: err.message }); } }); @@ -1070,14 +972,15 @@ app.delete('/api/financeiro/:id', async (req, res) => { // --- LEADS --- app.get('/api/leads', async (req, res) => { try { - const [rows] = await pool.query('SELECT * FROM leads'); + const { rows } = await pool.query('SELECT * FROM leads'); res.json(rows); } catch (err) { res.status(500).json({ error: err.message }); } }); app.post('/api/leads', async (req, res) => { try { - await pool.query('INSERT INTO leads SET ?', req.body); + const ins = buildInsert('leads', req.body); + await pool.query(ins.text, ins.values); res.json({ success: true }); } catch (err) { res.status(500).json({ error: err.message }); } }); @@ -1102,48 +1005,54 @@ app.get('/api/guias', async (req, res) => { let where = ' WHERE 1=1'; const params = []; + let paramIdx = 1; if (gto) { - where += ' AND (numeroGuiaPrestador LIKE ? OR numeroGuiaOperadora LIKE ?)'; + where += ` AND ("numeroGuiaPrestador" LIKE $${paramIdx} OR "numeroGuiaOperadora" LIKE $${paramIdx + 1})`; params.push(`%${gto}%`, `%${gto}%`); + paramIdx += 2; } if (search) { - where += ' AND (beneficiarioNome LIKE ? OR beneficiarioIdentificacao LIKE ?)'; + where += ` AND ("beneficiarioNome" LIKE $${paramIdx} OR "beneficiarioIdentificacao" LIKE $${paramIdx + 1})`; params.push(`%${search}%`, `%${search}%`); + paramIdx += 2; } if (status) { - where += ' AND status = ?'; + where += ` AND status = $${paramIdx}`; params.push(status); + paramIdx++; } if (dataInicio) { - where += ' AND dataSolicitacao >= ?'; + where += ` AND "dataSolicitacao" >= $${paramIdx}`; params.push(dataInicio); + paramIdx++; } if (dataFim) { - where += ' AND dataSolicitacao <= ?'; + where += ` AND "dataSolicitacao" <= $${paramIdx}`; params.push(dataFim); + paramIdx++; } // Validate sortField and sortOrder to prevent SQL injection const allowedSortFields = ['dataSolicitacao', 'beneficiarioNome', 'status', 'numeroGuiaPrestador']; - const finalSortField = allowedSortFields.includes(sortField) ? sortField : 'dataSolicitacao'; + const finalSortField = allowedSortFields.includes(sortField) ? `"${sortField}"` : '"dataSolicitacao"'; const finalSortOrder = sortOrder.toUpperCase() === 'ASC' ? 'ASC' : 'DESC'; const query = ` - SELECT * FROM guias_odontologicas - ${where} - ORDER BY ${finalSortField} ${finalSortOrder} - LIMIT ? OFFSET ? + SELECT * FROM guias_odontologicas + ${where} + ORDER BY ${finalSortField} ${finalSortOrder} + LIMIT $${paramIdx} OFFSET $${paramIdx + 1} `; const countQuery = `SELECT COUNT(*) as total FROM guias_odontologicas ${where}`; - const [rows] = await pool.query(query, [...params, limit, offset]); - const [totalRows] = await pool.query(countQuery, params); + const { rows } = await pool.query(query, [...params, limit, offset]); + const { rows: totalRows } = await pool.query(countQuery, params); res.json({ data: rows, @@ -1162,7 +1071,7 @@ app.post('/api/gto-builder', async (req, res) => { const { pacienteId, dentistaId, credenciadaId } = req.body; const id = Math.random().toString(36).substring(2, 9).toUpperCase(); await pool.query( - "INSERT INTO gto_builders (id, pacienteId, dentistaId, credenciadaId, status, total) VALUES ($1, $2, $3, $4, 'RASCUNHO', 0)", + "INSERT INTO gto_builders (id, \"pacienteId\", \"dentistaId\", \"credenciadaId\", status, total) VALUES ($1, $2, $3, $4, 'RASCUNHO', 0)", [id, pacienteId, dentistaId, credenciadaId] ); res.json({ id }); @@ -1171,8 +1080,8 @@ app.post('/api/gto-builder', async (req, res) => { app.get('/api/gto-builder/:id', async (req, res) => { try { - const [builder] = await pool.query('SELECT * FROM gto_builders WHERE id = ?', [req.params.id]); - const [items] = await pool.query('SELECT * FROM gto_items WHERE builderId = ?', [req.params.id]); + const { rows: builder } = await pool.query('SELECT * FROM gto_builders WHERE id = $1', [req.params.id]); + const { rows: items } = await pool.query('SELECT * FROM gto_items WHERE "builderId" = $1', [req.params.id]); res.json({ ...builder[0], items }); } catch (err) { res.status(500).json({ error: err.message }); } }); @@ -1181,11 +1090,12 @@ app.post('/api/gto-builder/:id/item', async (req, res) => { try { const itemId = Math.random().toString(36).substring(2, 9).toUpperCase(); const item = { ...req.body, id: itemId, builderId: req.params.id }; - await pool.query('INSERT INTO gto_items SET ?', item); + const ins = buildInsert('gto_items', item); + await pool.query(ins.text, ins.values); // Update total await pool.query( - 'UPDATE gto_builders SET total = (SELECT SUM(valorTotal) FROM gto_items WHERE builderId = ?) WHERE id = ?', + 'UPDATE gto_builders SET total = (SELECT SUM("valorTotal") FROM gto_items WHERE "builderId" = $1) WHERE id = $2', [req.params.id, req.params.id] ); @@ -1195,10 +1105,10 @@ app.post('/api/gto-builder/:id/item', async (req, res) => { app.delete('/api/gto-builder/:id/item/:itemId', async (req, res) => { try { - await pool.query('DELETE FROM gto_items WHERE id = ?', [req.params.itemId]); + await pool.query('DELETE FROM gto_items WHERE id = $1', [req.params.itemId]); // Update total await pool.query( - 'UPDATE gto_builders SET total = COALESCE((SELECT SUM(valorTotal) FROM gto_items WHERE builderId = ?), 0) WHERE id = ?', + 'UPDATE gto_builders SET total = COALESCE((SELECT SUM("valorTotal") FROM gto_items WHERE "builderId" = $1), 0) WHERE id = $2', [req.params.id, req.params.id] ); res.json({ success: true }); @@ -1222,7 +1132,7 @@ async function generateIntelligentNotifications() { try { // 1. Check for upcoming appointments (next 2 hours) const { rows: upcoming } = await pool.query(` - SELECT a.*, p.nome as "pacienteNome" + SELECT a.*, p.nome as "pacienteNome" FROM agendamentos a LEFT JOIN pacientes p ON a.pacientenome = p.nome WHERE a.start_time BETWEEN NOW() AND NOW() + INTERVAL '2 hours' @@ -1244,7 +1154,7 @@ async function generateIntelligentNotifications() { // 2. Check for overdue bills const { rows: overdue } = await pool.query(` - SELECT * FROM financeiro + SELECT * FROM financeiro WHERE datavencimento < CURRENT_DATE AND status = 'Pendente' `); @@ -1263,7 +1173,7 @@ async function generateIntelligentNotifications() { // 3. Check for bills due today const { rows: dueToday } = await pool.query(` - SELECT * FROM financeiro + SELECT * FROM financeiro WHERE datavencimento = CURRENT_DATE AND status = 'Pendente' `); @@ -1307,4 +1217,3 @@ app.listen(PORT, '0.0.0.0', () => { generateIntelligentNotifications(); setInterval(generateIntelligentNotifications, 5 * 60 * 1000); }); -