diff --git a/backend/server.js b/backend/server.js index 0cf3228..2d63601 100644 --- a/backend/server.js +++ b/backend/server.js @@ -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) { diff --git a/frontend/services/backend.ts b/frontend/services/backend.ts index 9660129..c8dede5 100644 --- a/frontend/services/backend.ts +++ b/frontend/services/backend.ts @@ -215,6 +215,15 @@ export const HybridBackend = { return res.json(); }, + // Importa SOMENTE pacientes de uma planilha EXISTENTE (ID/URL), sem alterar o backup. + importPacientesDaPlanilha: async (sheetsId: string): Promise => { + const clinicaId = HybridBackend.getActiveWorkspace()?.id || ''; + const res = await apiFetch(`${API_URL}/clinica-sync/import-pacientes`, { + method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ clinicaId, sheetsId }), + }); + return res.json(); + }, + // --- SUPERADMIN: MODO VISUALIZAÇÃO ("ver como" perfil, sem dados de terceiros) --- // Cria um workspace sintético com a role escolhida. Não há clínica real associada // (id inexistente), então as telas escopadas por clínica vêm vazias — nenhum dado diff --git a/frontend/views/ConfiguracoesView.tsx b/frontend/views/ConfiguracoesView.tsx index cce3a52..467d90d 100644 --- a/frontend/views/ConfiguracoesView.tsx +++ b/frontend/views/ConfiguracoesView.tsx @@ -204,8 +204,9 @@ const BackupSheetsCard: React.FC = () => { const toast = useToast(); const [loading, setLoading] = useState(true); const [st, setSt] = useState(null); - const [busy, setBusy] = useState<'' | 'push' | 'pull'>(''); + const [busy, setBusy] = useState<'' | 'push' | 'pull' | 'setid'>(''); const [logs, setLogs] = useState([]); + const [sheetIdInput, setSheetIdInput] = useState(''); const load = async () => { setLoading(true); @@ -213,6 +214,19 @@ const BackupSheetsCard: React.FC = () => { }; useEffect(() => { load(); }, []); + const importarPlanilha = async () => { + if (!sheetIdInput.trim()) return; + if (!window.confirm('Importar pacientes desta planilha? (pacientes com mesmo id são atualizados; a planilha NÃO é alterada)')) return; + setBusy('setid'); setLogs([]); + try { + const r = await HybridBackend.importPacientesDaPlanilha(sheetIdInput.trim()); + setLogs(r.logs || []); + if (!r.success) { toast.error(r.message || 'ERRO AO IMPORTAR.'); return; } + toast.success(`${r.imported ?? 0} PACIENTES IMPORTADOS!`); + load(); + } catch { toast.error('ERRO AO IMPORTAR.'); } finally { setBusy(''); } + }; + const push = async () => { setBusy('push'); setLogs([]); try { @@ -223,7 +237,7 @@ const BackupSheetsCard: React.FC = () => { } catch { toast.error('ERRO NO BACKUP.'); } finally { setBusy(''); } }; const pull = async () => { - if (!window.confirm('RESTAURAR vai sobrescrever os dados desta unidade com os da planilha. Continuar?')) return; + if (!window.confirm('IMPORTAR vai trazer os dados da planilha para esta unidade (registros com mesmo id são sobrescritos). Continuar?')) return; setBusy('pull'); setLogs([]); try { const r = await HybridBackend.clinicaSyncPull(); @@ -258,12 +272,28 @@ const BackupSheetsCard: React.FC = () => { ))} {st.lastSync?.lastPush &&

Último backup: {new Date(st.lastSync.lastPush).toLocaleString('pt-BR')}

} + + {/* Importar de uma planilha EXISTENTE (cole o ID ou o link) */} +
+

Importar de uma planilha existente

+

Cole o ID ou o link da planilha. Precisa ter uma aba PACIENTES com as colunas id, nome, cpf, telefone, email, ultimoTratamento, convenio, dataNascimento. Compartilhe a planilha com {st.googleEmail}. (A planilha não é alterada.)

+
+ setSheetIdInput(e.target.value)} + placeholder="ID ou link da planilha do Google" + className="flex-1 min-w-0 border border-blue-200 rounded-lg px-3 py-2 text-xs font-medium focus:outline-none focus:border-blue-400" /> + +
+
+
{logs.length > 0 && (