diff --git a/frontend/App.tsx b/frontend/App.tsx index 17ea641..463c168 100644 --- a/frontend/App.tsx +++ b/frontend/App.tsx @@ -199,7 +199,8 @@ const BOTTOM_TABS: Record { const permissions: Record = { paciente: ['tratamentos', 'notificacoes', 'configuracoes'], dentista: ['dashboard', 'agenda', 'ortodontia', 'tratamentos', 'lancar-gto', 'notificacoes', 'clinicas', 'configuracoes', 'plugins', 'rx-radiografias', 'salas', 'marketplace-profissionais', 'candidatura-tutor', 'tutoria-painel', 'diretorio-profissionais'], - biomedico: ['dashboard', 'agenda', 'ortodontia', 'tratamentos', 'lancar-gto', 'notificacoes', 'clinicas', 'configuracoes', 'plugins', 'rx-radiografias', 'salas', 'marketplace-profissionais', 'diretorio-profissionais'], + biomedico: ['dashboard', 'agenda', 'tratamentos', 'lancar-gto', 'notificacoes', 'clinicas', 'configuracoes', 'plugins', 'salas', 'marketplace-profissionais', 'diretorio-profissionais'], + protetico: ['dashboard', 'agenda', 'tratamentos', 'lancar-gto', 'notificacoes', 'clinicas', 'configuracoes', 'plugins', 'salas', 'marketplace-profissionais', 'diretorio-profissionais'], funcionario: ['dashboard', 'leads', 'pacientes', 'agenda', 'financeiro', 'tratamentos', 'lancar-gto', 'relatorios', 'notificacoes', 'clinicas', 'configuracoes', 'contratos', 'plugins', 'rx-radiografias', 'salas', 'marketplace-profissionais', 'candidatura-tutor', 'diretorio-profissionais'], }; diff --git a/frontend/components/Sidebar.tsx b/frontend/components/Sidebar.tsx index 3a3940b..f02a741 100644 --- a/frontend/components/Sidebar.tsx +++ b/frontend/components/Sidebar.tsx @@ -26,8 +26,9 @@ export const Sidebar: React.FC = ({ activeTab, setActiveTab }) => const pluginMenuItems: Array<{ id: string; label: string; icon: any; color: string }> = []; if (activePluginIds.includes('rx-scoreodonto')) { - // Visualização operacional de radiografias: papéis de clínica (não o superadmin). - if (currentRole !== 'superadmin') { + // Radiografias são ODONTOLÓGICAS: só papéis odonto (dentista + donos/funcionário). + // Biomédico e protético NÃO veem RX. + if (['dentista', 'funcionario', 'admin', 'donoclinica', 'donoconsultorio'].includes(currentRole)) { pluginMenuItems.push({ id: 'rx-radiografias', label: 'RX / RADIOGRAFIAS', icon: ScanLine, color: '#7c3aed' }); } // DISPOSITIVOS RX (config + PCs): admin/donos da clínica e superadmin (gestão global). @@ -106,7 +107,12 @@ export const Sidebar: React.FC = ({ activeTab, setActiveTab }) => if (['admin', 'donoclinica', 'donoconsultorio'].includes(currentRole)) return true; if (currentRole === 'paciente') return ['tratamentos', 'notificacoes', 'configuracoes'].includes(item.id); - if (currentRole === 'dentista' || currentRole === 'biomedico') return ['dashboard', 'tratamentos', 'agenda', 'ortodontia', 'notificacoes', 'clinicas', 'configuracoes', 'candidatura-tutor', 'diretorio-profissionais'].includes(item.id); + // Dentista: clínico odonto completo (ortodontia, tutoria orto). + if (currentRole === 'dentista') return ['dashboard', 'tratamentos', 'agenda', 'ortodontia', 'notificacoes', 'clinicas', 'configuracoes', 'candidatura-tutor', 'diretorio-profissionais'].includes(item.id); + // Biomédico(a): SEM itens odontológicos (ortodontia, RX, tutoria orto). + if (currentRole === 'biomedico') return ['dashboard', 'agenda', 'tratamentos', 'notificacoes', 'clinicas', 'configuracoes', 'diretorio-profissionais'].includes(item.id); + // Protético: laboratório — agenda própria + GTO; sem ortodontia/RX/pacientes clínicos. + if (currentRole === 'protetico') return ['dashboard', 'agenda', 'tratamentos', 'lancar-gto', 'notificacoes', 'clinicas', 'configuracoes', 'diretorio-profissionais'].includes(item.id); if (currentRole === 'funcionario') return ['dashboard', 'leads', 'pacientes', 'tratamentos', 'lancar-gto', 'agenda', 'financeiro', 'relatorios', 'notificacoes', 'clinicas', 'configuracoes', 'contratos', 'diretorio-profissionais'].includes(item.id); return false; }); diff --git a/frontend/services/backend.ts b/frontend/services/backend.ts index 9b7d690..0c0f0ed 100644 --- a/frontend/services/backend.ts +++ b/frontend/services/backend.ts @@ -259,12 +259,6 @@ export const HybridBackend = { getPreviewRole: (): string | null => localStorage.getItem('SCOREODONTO_PREVIEW_ROLE'), enterPreview: (role: string) => { - // Na primeira entrada do preview, guarda os plugins reais do superadmin p/ restaurar no exit. - // (não sobrescreve o backup se já estiver previewando e só trocando de papel) - if (!localStorage.getItem('SCOREODONTO_PREVIEW_ROLE')) { - const prevPlugins = localStorage.getItem('SCOREODONTO_ACTIVE_PLUGINS'); - localStorage.setItem('SCOREODONTO_PREVIEW_PREV_PLUGINS', prevPlugins === null ? '__none__' : prevPlugins); - } const synthetic = { id: '00000000-0000-0000-0000-000000000000', // UUID inexistente → consultas retornam vazio nome: `VISUALIZAÇÃO: ${role.toUpperCase()}`, @@ -275,8 +269,8 @@ export const HybridBackend = { localStorage.setItem('SCOREODONTO_PREVIEW_ROLE', role); localStorage.setItem('SCOREODONTO_ACTIVE_WORKSPACE', JSON.stringify(synthetic)); localStorage.setItem('SCOREODONTO_WORKSPACES', JSON.stringify([synthetic])); - // Preview ativa os plugins para o superadmin ver/testar as telas (RX, Salas, Profissionais). - localStorage.setItem('SCOREODONTO_ACTIVE_PLUGINS', JSON.stringify(['rx-scoreodonto', 'locacao-salas', 'profissionais-marketplace'])); + // Os plugins ativos no preview vêm de getActivePlugins() (preview-aware): todos + // ficam ativos enquanto SCOREODONTO_PREVIEW_ROLE existir. Nada a mutar aqui. const path = role === 'paciente' ? '/tratamentos' : '/dashboard'; window.location.assign(path); }, @@ -285,11 +279,6 @@ export const HybridBackend = { localStorage.removeItem('SCOREODONTO_PREVIEW_ROLE'); localStorage.removeItem('SCOREODONTO_ACTIVE_WORKSPACE'); localStorage.removeItem('SCOREODONTO_WORKSPACES'); - // Restaura os plugins que o superadmin tinha antes do preview. - const prev = localStorage.getItem('SCOREODONTO_PREVIEW_PREV_PLUGINS'); - if (prev === null || prev === '__none__') localStorage.removeItem('SCOREODONTO_ACTIVE_PLUGINS'); - else localStorage.setItem('SCOREODONTO_ACTIVE_PLUGINS', prev); - localStorage.removeItem('SCOREODONTO_PREVIEW_PREV_PLUGINS'); window.location.assign('/superadmin/overview'); }, diff --git a/frontend/services/viacep.ts b/frontend/services/viacep.ts new file mode 100644 index 0000000..6618543 --- /dev/null +++ b/frontend/services/viacep.ts @@ -0,0 +1,27 @@ +// Busca de endereço por CEP via ViaCEP. Retorna os campos em MAIÚSCULAS +// (padrão de endereço do sistema). Retorna null se o CEP for inválido, +// não existir ou a API falhar — o chamador trata o feedback ao usuário. +export interface EnderecoCep { + logradouro: string; + bairro: string; + cidade: string; + estado: string; +} + +export async function buscarCep(cep: string): Promise { + const digits = (cep || '').replace(/\D/g, ''); + if (digits.length !== 8) return null; + try { + const r = await fetch(`https://viacep.com.br/ws/${digits}/json/`); + const d = await r.json(); + if (d.erro) return null; + return { + logradouro: (d.logradouro || '').toUpperCase(), + bairro: (d.bairro || '').toUpperCase(), + cidade: (d.localidade || '').toUpperCase(), + estado: (d.uf || '').toUpperCase(), + }; + } catch { + return null; + } +} diff --git a/frontend/views/ConfiguracoesView.tsx b/frontend/views/ConfiguracoesView.tsx index aee47c6..24e9db3 100644 --- a/frontend/views/ConfiguracoesView.tsx +++ b/frontend/views/ConfiguracoesView.tsx @@ -5,28 +5,12 @@ import { GoogleConnectButton } from '../components/GoogleConnectButton.tsx'; import { useToast } from '../contexts/ToastContext.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-blue-500 focus:ring-2 focus:ring-blue-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' }; -// Busca de endereço por CEP (ViaCEP) — retorna tudo em MAIÚSCULAS. -async function buscarCep(cep: string): Promise<{ logradouro: string; bairro: string; cidade: string; estado: string } | null> { - const digits = (cep || '').replace(/\D/g, ''); - if (digits.length !== 8) return null; - try { - const r = await fetch(`https://viacep.com.br/ws/${digits}/json/`); - const d = await r.json(); - if (d.erro) return null; - return { - logradouro: (d.logradouro || '').toUpperCase(), - bairro: (d.bairro || '').toUpperCase(), - cidade: (d.localidade || '').toUpperCase(), - estado: (d.uf || '').toUpperCase(), - }; - } catch { return null; } -} - // --- Card: Meu Perfil (Sistema) --- const MeuPerfilCard: React.FC<{ clinColor: string; role: string }> = ({ clinColor, role }) => { const toast = useToast(); diff --git a/frontend/views/PatientsView.tsx b/frontend/views/PatientsView.tsx index 4adc5b2..c28da7e 100644 --- a/frontend/views/PatientsView.tsx +++ b/frontend/views/PatientsView.tsx @@ -4,6 +4,7 @@ import { HybridBackend } from '../services/backend.ts'; import { Paciente, Dentista, Agendamento, Plano } from '../types.ts'; import { useHybridBackend } from '../hooks/useHybridBackend.ts'; import { useToast } from '../contexts/ToastContext.tsx'; +import { buscarCep } from '../services/viacep.ts'; const RELACOES_COMUNS = ['MÃE', 'PAI', 'FILHO', 'FILHA', 'IRMÃO', 'IRMÃ', 'CÔNJUGE', 'AVÔ', 'AVÓ', 'NETO', 'NETA', 'TIO', 'TIA', 'SOBRINHO', 'SOBRINHA', 'PRIMO', 'PRIMA', 'PADRASTO', 'MADRASTA', 'ENTEADO', 'ENTEADA']; @@ -637,7 +638,15 @@ const EditarPacienteModal: React.FC<{ patient: Paciente; onClose: () => void; on
- setForm(f => ({ ...f, cep: e.target.value }))} className={inp} placeholder="00000-000" maxLength={9} /> + { + const v = e.target.value; + setForm(f => ({ ...f, cep: v })); + if (v.replace(/\D/g, '').length === 8) { + const r = await buscarCep(v); + if (r) { setForm(f => ({ ...f, 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.'); + } + }} className={inp} placeholder="00000-000" maxLength={9} />
diff --git a/frontend/views/plugins/ProfissionaisPlugin.tsx b/frontend/views/plugins/ProfissionaisPlugin.tsx index 6d2f28d..5c4f631 100644 --- a/frontend/views/plugins/ProfissionaisPlugin.tsx +++ b/frontend/views/plugins/ProfissionaisPlugin.tsx @@ -6,6 +6,7 @@ import { import { PageHeader } from '../../components/PageHeader.tsx'; import { useToast } from '../../contexts/ToastContext.tsx'; import { HybridBackend } from '../../services/backend.ts'; +import { buscarCep } from '../../services/viacep.ts'; const API_URL = (window as any).__API_URL__ || ''; const UF_LIST = ['', 'AC', 'AL', 'AM', 'AP', 'BA', 'CE', 'DF', 'ES', 'GO', 'MA', 'MG', 'MS', 'MT', 'PA', 'PB', 'PE', 'PI', 'PR', 'RJ', 'RN', 'RO', 'RR', 'RS', 'SC', 'SE', 'SP', 'TO']; @@ -385,7 +386,15 @@ export const ProfissionaisPlugin: React.FC = () => {
- setP('cep', e.target.value)} placeholder="79000-000" /> + { + const v = e.target.value; + setP('cep', v); + if (v.replace(/\D/g, '').length === 8) { + const r = await buscarCep(v); + if (r) { setPerfil((prev: any) => ({ ...prev, bairro: r.bairro, cidade: r.cidade, estado: r.estado })); toast.success('LOCALIZAÇÃO PREENCHIDA PELO CEP!'); } + else toast.error('CEP NÃO ENCONTRADO.'); + } + }} placeholder="79000-000" />
diff --git a/frontend/views/plugins/SalasPlugin.tsx b/frontend/views/plugins/SalasPlugin.tsx index c40fb68..2ebef7c 100644 --- a/frontend/views/plugins/SalasPlugin.tsx +++ b/frontend/views/plugins/SalasPlugin.tsx @@ -6,6 +6,7 @@ import { import { PageHeader } from '../../components/PageHeader.tsx'; import { useToast } from '../../contexts/ToastContext.tsx'; import { HybridBackend } from '../../services/backend.ts'; +import { buscarCep } from '../../services/viacep.ts'; const API_URL = (window as any).__API_URL__ || ''; const UF_LIST = ['', 'AC', 'AL', 'AM', 'AP', 'BA', 'CE', 'DF', 'ES', 'GO', 'MA', 'MG', 'MS', 'MT', 'PA', 'PB', 'PE', 'PI', 'PR', 'RJ', 'RN', 'RO', 'RR', 'RS', 'SC', 'SE', 'SP', 'TO']; @@ -171,7 +172,15 @@ const SalaFormModal: React.FC<{ sala: Sala | null; onClose: () => void; onSaved:
- set('cep', e.target.value)} placeholder="79000-000" /> + { + const v = e.target.value; + set('cep', v); + if (v.replace(/\D/g, '').length === 8) { + const r = await buscarCep(v); + if (r) { setF(p => ({ ...p, endereco: 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.'); + } + }} placeholder="79000-000" />
diff --git a/frontend/views/plugins/pluginRegistry.ts b/frontend/views/plugins/pluginRegistry.ts index f3fa506..d9f5fed 100644 --- a/frontend/views/plugins/pluginRegistry.ts +++ b/frontend/views/plugins/pluginRegistry.ts @@ -1,9 +1,15 @@ export const ACTIVE_PLUGINS_KEY = 'SCOREODONTO_ACTIVE_PLUGINS'; export const PLUGIN_CONFIG_KEY = 'SCOREODONTO_PLUGIN_CONFIG'; +// "Visualizar como" (superadmin) ativa todos os plugins para teste, sem depender +// do cache em localStorage (que é reconciliado/limpo em outros fluxos). +const PREVIEW_PLUGINS = ['rx-scoreodonto', 'locacao-salas', 'profissionais-marketplace']; + export const getActivePlugins = (): string[] => { - try { return JSON.parse(localStorage.getItem(ACTIVE_PLUGINS_KEY) || '[]'); } - catch { return []; } + try { + if (localStorage.getItem('SCOREODONTO_PREVIEW_ROLE')) return [...PREVIEW_PLUGINS]; + return JSON.parse(localStorage.getItem(ACTIVE_PLUGINS_KEY) || '[]'); + } catch { return []; } }; export const isPluginActive = (id: string): boolean => getActivePlugins().includes(id);