diff --git a/backend/server.js b/backend/server.js index 7fd1ff7..221df79 100644 --- a/backend/server.js +++ b/backend/server.js @@ -728,8 +728,19 @@ 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'); - res.json(rows); + const { q } = req.query; + let text, params; + if (q && q.trim()) { + const term = `%${q.trim().toUpperCase()}%`; + text = `SELECT * FROM pacientes WHERE UPPER(nome) LIKE $1 OR cpf LIKE $1 ORDER BY nome LIMIT 100`; + params = [term]; + } else { + text = `SELECT * FROM pacientes ORDER BY nome LIMIT 50`; + params = []; + } + const { rows } = await pool.query(text, params); + const { rows: total } = await pool.query('SELECT COUNT(*) as total FROM pacientes'); + res.json({ data: rows, total: parseInt(total[0].total) }); } catch (err) { res.status(500).json({ error: err.message }); } @@ -1492,6 +1503,71 @@ app.post('/api/sync/pull', tenantGuard, async (req, res) => { } }); +// Import patients from Apps Script legacy sheet (PACIENTES-CONSULTT-CLINIC tab) +app.post('/api/sync/import-legacy', tenantGuard, async (req, res) => { + const logs = []; + try { + const sheets = await getSheetsClient(); + logs.push('[←] LENDO ABA PACIENTES-CONSULTT-CLINIC...'); + const realTab = await ensureSheetTab(sheets, 'PACIENTES-CONSULTT-CLINIC'); + const resp = await sheets.spreadsheets.values.get({ + spreadsheetId: SHEETS_BACKUP_ID, + range: `${realTab}!A:ZZ` + }); + const sheetRows = resp.data.values || []; + if (sheetRows.length < 2) { + logs.push('[AVISO] ABA VAZIA OU SEM DADOS.'); + return res.json({ success: true, logs, imported: 0 }); + } + const headers = sheetRows[0].map(h => h.trim()); + const dataRows = sheetRows.slice(1).filter(r => r.some(c => c && c.trim() !== '')); + logs.push(`[OK] ${dataRows.length} LINHAS ENCONTRADAS`); + + const idx = h => headers.indexOf(h); + const iNome = idx('Nome'); const iSobre = idx('SobreNome'); + const iCpf = idx('CPF'); const iCred = idx('Credenciada'); + const iNasci = idx('DataNasci'); const iTel = idx('TefefonePrincipal'); + + let imported = 0, skipped = 0; + for (let i = 0; i < dataRows.length; i++) { + const row = dataRows[i]; + const get = j => (j >= 0 && j < row.length ? (row[j] || '').trim() : ''); + const nome = [get(iNome), get(iSobre)].filter(Boolean).join(' ').toUpperCase(); + if (!nome) { skipped++; continue; } + const cpfRaw = get(iCpf); + const cpfDigits = cpfRaw.replace(/\D/g, ''); + const id = cpfDigits || `legacy_${i + 1}`; + try { + const trunc = (v, n) => v ? v.substring(0, n) : null; + await pool.query( + `INSERT INTO pacientes (id,nome,cpf,telefone,email,ultimotratamento,convenio,datanascimento) + VALUES ($1,$2,$3,$4,$5,$6,$7,$8) + ON CONFLICT (id) DO UPDATE SET + nome=EXCLUDED.nome,cpf=EXCLUDED.cpf,telefone=EXCLUDED.telefone, + convenio=EXCLUDED.convenio,datanascimento=EXCLUDED.datanascimento`, + [trunc(id,50), trunc(nome,255), trunc(cpfRaw,20)||null, trunc(get(iTel),20)||null, null, null, trunc(get(iCred),100)||null, trunc(get(iNasci),20)||null] + ); + imported++; + } catch (e) { + skipped++; + if (skipped <= 3) logs.push(`[AVISO] linha ${i + 2}: ${e.message}`); + } + } + logs.push(`[✓] ${imported} PACIENTES IMPORTADOS, ${skipped} IGNORADOS`); + if (imported > 0) { + await saveSyncStatus({ pacientes: { lastPush: new Date().toISOString(), count: imported, source: 'legacy' } }); + schedulePush('pacientes'); + } + res.json({ success: true, logs, imported, skipped }); + } catch (err) { + const msg = err.message === 'NO_GOOGLE_TOKEN' + ? 'NENHUMA CONTA GOOGLE CONECTADA.' + : err.message; + logs.push(`[ERRO] ${msg}`); + res.status(err.message === 'NO_GOOGLE_TOKEN' ? 400 : 500).json({ success: false, logs }); + } +}); + // initTables() was removed as database is native PostgreSQL and schema DDL is maintained externally. async function generateIntelligentNotifications() { if (!pool) return;