import React, { useState, useEffect } from 'react'; import { User, Building2, ShieldCheck, LogOut, KeyRound, CheckCircle2, AlertCircle, Eye, EyeOff, Loader2, Pencil, X, Upload, Download } from 'lucide-react'; import { HybridBackend } from '../services/backend.ts'; import { GoogleConnectButton } from '../components/GoogleConnectButton.tsx'; import { useToast } from '../contexts/ToastContext.tsx'; import { useConfirm } from '../contexts/ConfirmContext.tsx'; import { ChangePasswordModal } from '../components/ChangePasswordModal.tsx'; import { APP_VERSION } from '../constants.ts'; import { buscarCep } from '../services/viacep.ts'; const FIELD_CLS = 'w-full border border-gray-200 rounded-xl px-3 py-2.5 text-sm font-medium text-gray-800 outline-none focus:border-teal-500 focus:ring-2 focus:ring-teal-100 transition-all'; const LABEL_CLS = 'text-[10px] font-black text-gray-400 uppercase tracking-wider block mb-1'; const TIPO_LABEL: Record = { pessoal: 'Pessoal', consultorio: 'Consultório', clinica: 'Clínica' }; // --- Card: Meu Perfil (Sistema) --- const MeuPerfilCard: React.FC<{ clinColor: string; role: string }> = ({ clinColor, role }) => { const toast = useToast(); const confirm = useConfirm(); const [loading, setLoading] = useState(true); const [editing, setEditing] = useState(false); const [saving, setSaving] = useState(false); const [f, setF] = useState({}); const load = async () => { setLoading(true); try { const r = await HybridBackend.getMeuPerfil(); if (r.success) setF(r.usuario || {}); } catch { /* ignore */ } finally { setLoading(false); } }; useEffect(() => { load(); }, []); const set = (k: string, v: any) => setF((p: any) => ({ ...p, [k]: v })); const save = async () => { if (!f.nome?.trim()) { toast.error('NOME OBRIGATÓRIO.'); return; } setSaving(true); try { const r = await HybridBackend.updateMeuPerfil({ nome: f.nome, email: f.email, celular: f.celular, cro: f.cro, cro_uf: f.cro_uf, especialidade: f.especialidade, cidade: f.cidade, bairro: f.bairro, estado: f.estado, }); if (!r.success) { toast.error(r.message || 'ERRO AO SALVAR.'); return; } toast.success('PERFIL ATUALIZADO!'); setEditing(false); } catch { toast.error('ERRO AO SALVAR.'); } finally { setSaving(false); } }; return (

Meu Perfil (Sistema)

{!editing && !loading && ( )}
{loading ? (
) : !editing ? (

{f.nome || '—'}

{f.email || '—'}

{f.celular && {f.celular}} {f.cro && CRO {f.cro_uf}/{f.cro}} {f.especialidade && {f.especialidade}}
{role}
) : (
set('nome', e.target.value.toUpperCase())} />
set('email', e.target.value)} />
set('celular', e.target.value)} placeholder="(00) 00000-0000" />
set('especialidade', e.target.value.toUpperCase())} />
set('cro', e.target.value)} />
set('cro_uf', e.target.value.toUpperCase())} />
set('cidade', e.target.value.toUpperCase())} />
set('estado', e.target.value.toUpperCase())} />
)}
); }; // --- Card: Perfil da Unidade (clínica/consultório/laboratório) --- const UnidadePerfilCard: React.FC<{ workspaceId?: string; clinColor: string }> = ({ workspaceId, clinColor }) => { const toast = useToast(); const confirm = useConfirm(); const [loading, setLoading] = useState(true); const [editing, setEditing] = useState(false); const [saving, setSaving] = useState(false); const [f, setF] = useState({}); const [allAreas, setAllAreas] = useState<{ slug: string; nome: string; cor: string }[]>([]); const [clinAreas, setClinAreas] = useState([]); useEffect(() => { HybridBackend.getAreas().then(setAllAreas); }, []); const load = async () => { if (!workspaceId) { setLoading(false); return; } setLoading(true); try { const r = await HybridBackend.getClinicaProfile(workspaceId); if (r.success) setF(r.clinica || {}); setClinAreas(await HybridBackend.getClinicaAreas(workspaceId)); } catch { /* ignore */ } finally { setLoading(false); } }; useEffect(() => { load(); }, [workspaceId]); const set = (k: string, v: any) => setF((p: any) => ({ ...p, [k]: v })); const [buscandoCep, setBuscandoCep] = useState(false); const handleCep = async (v: string) => { set('cep', v); if (v.replace(/\D/g, '').length === 8) { setBuscandoCep(true); const r = await buscarCep(v); setBuscandoCep(false); if (r) { setF((p: any) => ({ ...p, logradouro: r.logradouro, bairro: r.bairro, cidade: r.cidade, estado: r.estado })); toast.success('ENDEREÇO PREENCHIDO PELO CEP!'); } else toast.error('CEP NÃO ENCONTRADO.'); } }; const save = async () => { if (!f.nome_fantasia?.trim()) { toast.error('NOME DA UNIDADE OBRIGATÓRIO.'); return; } if (!workspaceId) return; setSaving(true); try { const r = await HybridBackend.updateClinicaProfile(workspaceId, { nome_fantasia: f.nome_fantasia, documento: f.documento, telefone: f.telefone, cor: f.cor, logradouro: f.logradouro, numero: f.numero, bairro: f.bairro, cidade: f.cidade, estado: f.estado, cep: f.cep, }); if (!r.success) { toast.error(r.message || 'ERRO AO SALVAR.'); return; } await HybridBackend.setClinicaAreas(workspaceId, clinAreas); toast.success('DADOS DA UNIDADE ATUALIZADOS!'); setEditing(false); } catch { toast.error('ERRO AO SALVAR.'); } finally { setSaving(false); } }; const endereco = [f.logradouro, f.numero, f.bairro, f.cidade && `${f.cidade}${f.estado ? '/' + f.estado : ''}`].filter(Boolean).join(', '); return (

Perfil da Unidade {f.tipo ? `· ${TIPO_LABEL[f.tipo] || f.tipo}` : ''}

{!editing && !loading && workspaceId && ( )}
{loading ? (
) : !workspaceId ? (

Sem unidade vinculada

) : !editing ? (

{f.nome_fantasia || '—'}

{f.documento && Doc: {f.documento}} {f.telefone && {f.telefone}}
{endereco &&

{endereco}{f.cep ? ` — ${f.cep}` : ''}

}

ID: {f.id}

) : (
set('nome_fantasia', e.target.value.toUpperCase())} />
set('documento', e.target.value)} placeholder="00.000.000/0000-00" />
set('telefone', e.target.value)} placeholder="(00) 00000-0000" />
handleCep(e.target.value)} placeholder="00000-000" />
set('numero', e.target.value)} />
set('logradouro', e.target.value.toUpperCase())} />
set('bairro', e.target.value.toUpperCase())} />
set('cidade', e.target.value.toUpperCase())} />
set('estado', e.target.value.toUpperCase())} />
set('cor', e.target.value)} />
{allAreas.map(a => { const on = clinAreas.includes(a.slug); return ( ); })}

Uma unidade pode ter mais de uma área (ex.: odontologia + biomedicina).

)}
); }; // --- Card: Backup por clínica (Google Sheets) --- const BackupSheetsCard: React.FC = () => { const toast = useToast(); const confirm = useConfirm(); const [loading, setLoading] = useState(true); const [st, setSt] = useState(null); const [busy, setBusy] = useState<'' | 'push' | 'pull' | 'setid' | 'export'>(''); const [logs, setLogs] = useState([]); const [sheetIdInput, setSheetIdInput] = useState(''); const exportarCsv = async () => { setBusy('export'); try { await HybridBackend.exportPacientesCsv(); toast.success('BACKUP (.CSV) BAIXADO!'); } catch { toast.error('ERRO AO EXPORTAR.'); } finally { setBusy(''); } }; const load = async () => { setLoading(true); try { setSt(await HybridBackend.getClinicaSyncStatus()); } catch { /* ignore */ } finally { setLoading(false); } }; useEffect(() => { load(); }, []); const importarPlanilha = async () => { if (!sheetIdInput.trim()) return; if (!await 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 { const r = await HybridBackend.clinicaSyncPush(); setLogs(r.logs || []); if (!r.success) { toast.error(r.message || 'ERRO NO BACKUP.'); return; } toast.success('BACKUP ENVIADO!'); load(); } catch { toast.error('ERRO NO BACKUP.'); } finally { setBusy(''); } }; const pull = async () => { if (!await 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(); setLogs(r.logs || []); if (!r.success) { toast.error(r.message || 'ERRO AO RESTAURAR.'); return; } toast.success('DADOS RESTAURADOS!'); load(); } catch { toast.error('ERRO AO RESTAURAR.'); } finally { setBusy(''); } }; return (

Backup · Google Sheets

{st?.spreadsheetUrl && Abrir planilha ↗}
{/* Exportar pacientes (.csv) — sempre disponível, não depende do Google */}

Backup dos pacientes (.csv)

Baixa todos os pacientes desta unidade. Guarde para contingência (ex.: usar no seu outro sistema se o servidor cair).

{loading ? (
) : !st?.connected ? (
Conecte a conta Google desta unidade na Agenda → Configurações (engrenagem) para habilitar o backup. A planilha é criada automaticamente no Drive dessa conta.
) : ( <>

Conta Google desta unidade: {st.googleEmail}

{Object.entries(st.counts || {}).map(([k, v]: any) => (

{v}

{k}

))}
{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-teal-200 rounded-lg px-3 py-2 text-xs font-medium focus:outline-none focus:border-teal-400" />
{logs.length > 0 && (
{logs.join('\n')}
)} )}
); }; export const ConfiguracoesView: React.FC = () => { const [connectedAccounts, setConnectedAccounts] = useState([]); const [credsStatus, setCredsStatus] = useState<{ configured: boolean; clientIdHint: string | null; fromEnv: boolean } | null>(null); const [clientId, setClientId] = useState(''); const [clientSecret, setClientSecret] = useState(''); const [showSecret, setShowSecret] = useState(false); const [savingCreds, setSavingCreds] = useState(false); const currentRole = HybridBackend.getCurrentRole(); const userName = HybridBackend.getCurrentUserName(); const userEmail = HybridBackend.getCurrentUser(); const activeWorkspace = HybridBackend.getActiveWorkspace(); const clinColor = activeWorkspace?.cor || '#2563eb'; const toast = useToast(); const confirm = useConfirm(); const [showChangePw, setShowChangePw] = useState(false); const authHeader = { 'Authorization': `Bearer ${localStorage.getItem('SCOREODONTO_AUTH_TOKEN')}`, 'Content-Type': 'application/json' }; const fetchGoogleStatus = async () => { try { const clinicaId = activeWorkspace?.id || ''; const res = await fetch(`/api/auth/google/status${clinicaId ? `?clinicaId=${clinicaId}` : ''}`); setConnectedAccounts(await res.json()); } catch {} }; const fetchCredsStatus = async () => { try { const res = await fetch('/api/settings/google-credentials', { headers: authHeader }); if (res.ok) setCredsStatus(await res.json()); } catch {} }; useEffect(() => { fetchGoogleStatus(); if (currentRole === 'superadmin') fetchCredsStatus(); }, []); const handleSaveCreds = async () => { if (!clientId.trim() || !clientSecret.trim()) { toast.error('PREENCHA OS DOIS CAMPOS.'); return; } setSavingCreds(true); try { const res = await fetch('/api/settings/google-credentials', { method: 'POST', headers: authHeader, body: JSON.stringify({ clientId: clientId.trim(), clientSecret: clientSecret.trim() }) }); if (res.ok) { toast.success('CREDENCIAIS SALVAS!'); setClientId(''); setClientSecret(''); await fetchCredsStatus(); } else { const d = await res.json(); toast.error(d.error || 'ERRO AO SALVAR.'); } } catch { toast.error('FALHA NA CONEXÃO.'); } finally { setSavingCreds(false); } }; const handleLogout = async () => { if (await confirm('TEM CERTEZA QUE DESEJA SAIR DO SISTEMA?')) { HybridBackend.logout(); } }; return (

Configurações

Gerencie seu perfil e integrações

{APP_VERSION}
{/* Perfil do sistema (usuário) */} {/* Perfil da unidade (clínica/consultório/laboratório) */} {/* Backup por clínica (Google Sheets) */} {activeWorkspace?.id && currentRole !== 'paciente' && } {/* Integrações — só para admin */} {currentRole === 'superadmin' && (

Integrações Google

{/* Credenciais OAuth */}
Credenciais OAuth 2.0
{credsStatus?.configured ? (
{credsStatus.fromEnv ? 'Via ENV' : 'Configurado'}
) : (
Não configurado
)}
{credsStatus?.configured && credsStatus.clientIdHint && !credsStatus.fromEnv && (

Client ID atual

{credsStatus.clientIdHint}

)} {credsStatus?.fromEnv && (

Credenciais carregadas via variável de ambiente. Para substituir, preencha os campos abaixo.

)}
setClientId(e.target.value)} placeholder={credsStatus?.configured ? 'Deixe vazio para manter atual' : 'xxxxxxxxxxxx.apps.googleusercontent.com'} className="w-full border border-gray-200 rounded-xl px-4 py-2.5 text-sm font-mono focus:ring-2 focus:ring-teal-100 focus:border-teal-300 outline-none transition-all" />
setClientSecret(e.target.value)} placeholder={credsStatus?.configured ? 'Deixe vazio para manter atual' : 'GOCSPX-...'} className="w-full border border-gray-200 rounded-xl px-4 py-2.5 pr-10 text-sm font-mono focus:ring-2 focus:ring-teal-100 focus:border-teal-300 outline-none transition-all" />

Obtenha as credenciais em{' '} console.cloud.google.com {' '}→ APIs & Services → Credentials → OAuth 2.0 Client ID. Adicione{' '} https://scoreodonto.com/api/oauth2callback {' '}como URI de redirecionamento autorizado.

{/* Conectar conta Google */}

Conta conectada

a.owner_id === (activeWorkspace?.id || 'admin') || a.owner_id === userEmail)} onStatusChange={fetchGoogleStatus} />
)} {/* Segurança */}

Segurança

Sessão ativa e autenticada
{showChangePw && setShowChangePw(false)} onDone={() => setShowChangePw(false)} />}
); };