feat(config): importar pacientes de planilha Google existente (somente-leitura)

- backend POST /api/clinica-sync/import-pacientes (le aba 'pacientes' de um ID/URL, nao altera sheets_id nem a planilha)
- endpoint set-id (opcional) para apontar planilha de backup
- frontend: campo de ID/URL + botao Importar no card Backup; rotulo Restaurar->Importar

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
VPS 4 Builder
2026-06-10 01:24:44 +02:00
parent e6a8e67997
commit 1e4f4dee2f
3 changed files with 91 additions and 3 deletions
+49
View File
@@ -1023,6 +1023,55 @@ app.post('/api/clinica-sync/pull', tenantGuard, async (req, res) => {
}
});
// Importa SOMENTE pacientes de uma planilha EXISTENTE (ID/URL), sem tocar no sheets_id
// de backup (não-destrutivo: nunca limpa/sobrescreve a planilha fonte). Lê a aba "pacientes".
app.post('/api/clinica-sync/import-pacientes', tenantGuard, async (req, res) => {
const clinicaId = req.body?.clinicaId || req.query.clinicaId;
let sheetsId = String(req.body?.sheetsId || '').trim();
const logs = [];
try {
if (!clinicaId || !sheetsId) return res.status(400).json({ success: false, message: 'clinicaId e ID da planilha são obrigatórios.' });
const m = sheetsId.match(/\/d\/([a-zA-Z0-9-_]+)/); if (m) sheetsId = m[1];
const sheets = await getClinicSheetsClient(clinicaId);
let title;
try { const meta = await sheets.spreadsheets.get({ spreadsheetId: sheetsId, fields: 'properties.title' }); title = meta.data.properties.title; }
catch { return res.status(400).json({ success: false, message: 'Não consegui abrir essa planilha. Confira o ID e se ela está compartilhada com a conta Google desta unidade.' }); }
logs.push(`[←] LENDO "${title}" → ABA PACIENTES...`);
const n = await pullClinicTable(CLINIC_SYNC_TABLES[0], sheets, sheetsId, clinicaId); // [0] = pacientes
logs.push(`[✓] ${n} PACIENTES IMPORTADOS`);
res.json({ success: true, imported: n, logs });
} catch (err) {
const msg = err.message === 'NO_GOOGLE_TOKEN' ? 'CONECTE A CONTA GOOGLE DESTA UNIDADE PRIMEIRO (AGENDA → CONFIGURAÇÕES).' : err.message;
res.status(400).json({ success: false, message: msg, logs: [...logs, `[ERRO] ${msg}`] });
}
});
// Define a planilha (ID/URL) a ser usada por esta clínica → habilita IMPORTAR de uma
// planilha EXISTENTE (ex.: migração de pacientes). Valida o acesso com a conta da unidade.
app.post('/api/clinica-sync/set-id', tenantGuard, async (req, res) => {
const clinicaId = req.body?.clinicaId || req.query.clinicaId;
let sheetsId = String(req.body?.sheetsId || '').trim();
try {
if (!clinicaId || !sheetsId) return res.status(400).json({ success: false, message: 'clinicaId e ID da planilha são obrigatórios.' });
const m = sheetsId.match(/\/d\/([a-zA-Z0-9-_]+)/); // aceita URL completa
if (m) sheetsId = m[1];
const sheets = await getClinicSheetsClient(clinicaId);
let title, abas = [];
try {
const meta = await sheets.spreadsheets.get({ spreadsheetId: sheetsId, fields: 'properties.title,sheets.properties.title' });
title = meta.data.properties.title;
abas = (meta.data.sheets || []).map(s => s.properties.title);
} catch {
return res.status(400).json({ success: false, message: 'Não consegui abrir essa planilha. Confira o ID e se ela está compartilhada com a conta Google desta unidade (' + sheetsId + ').' });
}
await pool.query('UPDATE clinicas SET sheets_id = $1 WHERE id = $2', [sheetsId, clinicaId]);
res.json({ success: true, spreadsheetId: sheetsId, spreadsheetUrl: clinicSheetUrl(sheetsId), title, abas });
} catch (err) {
const msg = err.message === 'NO_GOOGLE_TOKEN' ? 'CONECTE A CONTA GOOGLE DESTA UNIDADE PRIMEIRO (AGENDA → CONFIGURAÇÕES).' : err.message;
res.status(400).json({ success: false, message: msg });
}
});
// --- GOOGLE AUTH ROUTES ---
app.get('/api/auth/google/url', (req, res) => {
if (!googleCreds.clientId || !googleCreds.clientSecret) {