Merge branch 'claude/youthful-mendel-0c569f'
This commit is contained in:
+10
-5
@@ -23,6 +23,7 @@ import { DentistRegisterView } from './views/DentistRegisterView.tsx';
|
||||
import { DentistasView, EspecialidadesView, PlanosView, SyncView, ProcedimentosView } from './views/AdminViews.tsx';
|
||||
import { NotificationsView } from './views/NotificationsView.tsx';
|
||||
import { ReportsView } from './views/ReportsView.tsx';
|
||||
import { ConfiguracoesView } from './views/ConfiguracoesView.tsx';
|
||||
|
||||
export type ViewKey =
|
||||
| 'landing'
|
||||
@@ -45,7 +46,8 @@ export type ViewKey =
|
||||
| 'clinicas'
|
||||
| 'sync'
|
||||
| 'relatorios'
|
||||
| 'cadastro-dentista';
|
||||
| 'cadastro-dentista'
|
||||
| 'configuracoes';
|
||||
|
||||
// ------- URL <-> VIEW mapping -------
|
||||
const ROUTE_MAP: Record<string, ViewKey> = {
|
||||
@@ -71,6 +73,7 @@ const ROUTE_MAP: Record<string, ViewKey> = {
|
||||
'/notificacoes': 'notificacoes',
|
||||
'/relatorios': 'relatorios',
|
||||
'/cadastro-dentista': 'cadastro-dentista',
|
||||
'/configuracoes': 'configuracoes',
|
||||
};
|
||||
|
||||
const VIEW_TO_ROUTE: Record<ViewKey, string> = {
|
||||
@@ -94,7 +97,8 @@ const VIEW_TO_ROUTE: Record<ViewKey, string> = {
|
||||
notificacoes: '/notificacoes',
|
||||
relatorios: '/relatorios',
|
||||
sync: '/admin/sync',
|
||||
'cadastro-dentista': '/cadastro-dentista'
|
||||
'cadastro-dentista': '/cadastro-dentista',
|
||||
configuracoes: '/configuracoes',
|
||||
};
|
||||
|
||||
/** Resolve current URL to a ViewKey */
|
||||
@@ -121,9 +125,9 @@ const App: React.FC = () => {
|
||||
if (role === 'admin' || role === 'donoclinica') return true;
|
||||
|
||||
const permissions: Record<string, ViewKey[]> = {
|
||||
paciente: ['tratamentos', 'notificacoes'],
|
||||
dentista: ['dashboard', 'pacientes', 'agenda', 'ortodontia', 'tratamentos', 'notificacoes', 'clinicas'],
|
||||
funcionario: ['dashboard', 'leads', 'pacientes', 'agenda', 'financeiro', 'tratamentos', 'lancar-gto', 'relatorios', 'notificacoes', 'clinicas'],
|
||||
paciente: ['tratamentos', 'notificacoes', 'configuracoes'],
|
||||
dentista: ['dashboard', 'pacientes', 'agenda', 'ortodontia', 'tratamentos', 'notificacoes', 'clinicas', 'configuracoes'],
|
||||
funcionario: ['dashboard', 'leads', 'pacientes', 'agenda', 'financeiro', 'tratamentos', 'lancar-gto', 'relatorios', 'notificacoes', 'clinicas', 'configuracoes'],
|
||||
};
|
||||
|
||||
return permissions[role]?.includes(view) || false;
|
||||
@@ -225,6 +229,7 @@ const App: React.FC = () => {
|
||||
case 'notificacoes': return <NotificationsView />;
|
||||
case 'relatorios': return <ReportsView />;
|
||||
case 'cadastro-dentista': return <DentistRegisterView />;
|
||||
case 'configuracoes': return <ConfiguracoesView />;
|
||||
case 'login':
|
||||
default:
|
||||
return <LoginView onLoginSuccess={() => {
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Users, Calendar as CalendarIcon, LayoutDashboard, Stethoscope,
|
||||
DollarSign, RefreshCw, Database, Activity, BookOpen, Award, Megaphone, LogOut, ClipboardList, Bell, Building2, User, BarChart3
|
||||
DollarSign, RefreshCw, Database, Activity, BookOpen, Award, Megaphone, LogOut, ClipboardList, Bell, Building2, User, BarChart3, Settings
|
||||
} from 'lucide-react';
|
||||
import { HybridBackend } from '../services/backend.ts';
|
||||
import { GoogleConnectButton } from './GoogleConnectButton.tsx';
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
interface SidebarProps {
|
||||
activeTab: string;
|
||||
@@ -13,27 +11,12 @@ interface SidebarProps {
|
||||
}
|
||||
|
||||
export const Sidebar: React.FC<SidebarProps> = ({ activeTab, setActiveTab }) => {
|
||||
const [connectedAccounts, setConnectedAccounts] = useState<any[]>([]);
|
||||
const currentRole = HybridBackend.getCurrentRole();
|
||||
const userName = HybridBackend.getCurrentUserName();
|
||||
const userEmail = HybridBackend.getCurrentUser();
|
||||
const activeWorkspace = HybridBackend.getActiveWorkspace();
|
||||
const clinColor = activeWorkspace?.cor || '#2563eb';
|
||||
|
||||
const fetchGoogleStatus = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/auth/google/status');
|
||||
const data = await response.json();
|
||||
setConnectedAccounts(data);
|
||||
} catch (error) {
|
||||
console.error("Erro ao buscar status do Google:", error);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchGoogleStatus();
|
||||
}, []);
|
||||
|
||||
const menuItems = [
|
||||
{ id: 'dashboard', label: 'VISÃO GERAL', icon: LayoutDashboard },
|
||||
{ id: 'clinicas', label: 'MINHAS CLÍNICAS', icon: Building2 },
|
||||
@@ -51,11 +34,12 @@ export const Sidebar: React.FC<SidebarProps> = ({ activeTab, setActiveTab }) =>
|
||||
{ id: 'planos', label: 'PLANOS / CONVÊNIOS', icon: BookOpen },
|
||||
{ id: 'notificacoes', label: 'NOTIFICAÇÕES', icon: Bell },
|
||||
{ id: 'sync', label: 'SINCRONIZAÇÃO', icon: RefreshCw },
|
||||
{ id: 'configuracoes', label: 'CONFIGURAÇÕES', icon: Settings },
|
||||
].filter(item => {
|
||||
if (['admin', 'donoclinica'].includes(currentRole)) return true;
|
||||
if (currentRole === 'paciente') return ['tratamentos', 'notificacoes'].includes(item.id);
|
||||
if (currentRole === 'dentista') return ['dashboard', 'pacientes', 'tratamentos', 'agenda', 'ortodontia', 'notificacoes', 'clinicas'].includes(item.id);
|
||||
if (currentRole === 'funcionario') return ['dashboard', 'leads', 'pacientes', 'tratamentos', 'lancar-gto', 'agenda', 'financeiro', 'relatorios', 'notificacoes', 'clinicas'].includes(item.id);
|
||||
if (currentRole === 'paciente') return ['tratamentos', 'notificacoes', 'configuracoes'].includes(item.id);
|
||||
if (currentRole === 'dentista') return ['dashboard', 'pacientes', 'tratamentos', 'agenda', 'ortodontia', 'notificacoes', 'clinicas', 'configuracoes'].includes(item.id);
|
||||
if (currentRole === 'funcionario') return ['dashboard', 'leads', 'pacientes', 'tratamentos', 'lancar-gto', 'agenda', 'financeiro', 'relatorios', 'notificacoes', 'clinicas', 'configuracoes'].includes(item.id);
|
||||
return false;
|
||||
});
|
||||
|
||||
@@ -105,39 +89,23 @@ export const Sidebar: React.FC<SidebarProps> = ({ activeTab, setActiveTab }) =>
|
||||
})}
|
||||
</nav>
|
||||
|
||||
<div className="p-4 border-t border-gray-100 bg-gray-50/50 space-y-4">
|
||||
{/* User Profile Section */}
|
||||
<div className="bg-white p-3 rounded-2xl border border-gray-100 shadow-sm">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<div className="w-10 h-10 bg-gray-100 rounded-full flex items-center justify-center text-gray-500 border border-gray-200">
|
||||
<User size={20} />
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<p className="text-xs font-black text-gray-800 uppercase truncate leading-none mb-1">{userName}</p>
|
||||
<p className="text-[9px] font-bold text-gray-400 truncate">{userEmail}</p>
|
||||
</div>
|
||||
<div className="p-4 border-t border-gray-100 bg-gray-50/50 space-y-2">
|
||||
<button
|
||||
onClick={() => setActiveTab('configuracoes')}
|
||||
className="w-full flex items-center gap-3 px-3 py-2 rounded-xl transition-all group"
|
||||
style={{
|
||||
backgroundColor: activeTab === 'configuracoes' ? clinColor : 'transparent',
|
||||
}}
|
||||
>
|
||||
<div className="w-8 h-8 bg-gray-100 rounded-full flex items-center justify-center text-gray-500 border border-gray-200 flex-shrink-0">
|
||||
<User size={15} />
|
||||
</div>
|
||||
<div
|
||||
className="flex items-center gap-2 p-2 rounded-lg border transition-all duration-500"
|
||||
style={{
|
||||
backgroundColor: `${clinColor}08`,
|
||||
borderColor: `${clinColor}22`
|
||||
}}
|
||||
>
|
||||
<div className="w-2 h-2 rounded-full shadow-sm" style={{ backgroundColor: clinColor }}></div>
|
||||
<span className="text-[9px] font-black uppercase truncate" style={{ color: clinColor }}>
|
||||
{activeWorkspace?.nome || 'Sem Unidade'}
|
||||
</span>
|
||||
<div className="min-w-0 flex-1 text-left">
|
||||
<p className={`text-[10px] font-black uppercase truncate leading-none ${activeTab === 'configuracoes' ? 'text-white' : 'text-gray-700'}`}>{userName}</p>
|
||||
<p className={`text-[9px] font-medium truncate mt-0.5 ${activeTab === 'configuracoes' ? 'text-white/70' : 'text-gray-400'}`}>{userEmail}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{currentRole === 'admin' && (
|
||||
<GoogleConnectButton
|
||||
ownerId="admin"
|
||||
isConnected={connectedAccounts.some(a => a.owner_id === 'admin' || a.owner_id === userEmail)}
|
||||
onStatusChange={fetchGoogleStatus}
|
||||
/>
|
||||
)}
|
||||
<Settings size={14} className={activeTab === 'configuracoes' ? 'text-white' : 'text-gray-400'} />
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { User, Building2, ShieldCheck, LogOut } from 'lucide-react';
|
||||
import { HybridBackend } from '../services/backend.ts';
|
||||
import { GoogleConnectButton } from '../components/GoogleConnectButton.tsx';
|
||||
|
||||
export const ConfiguracoesView: React.FC = () => {
|
||||
const [connectedAccounts, setConnectedAccounts] = useState<any[]>([]);
|
||||
const currentRole = HybridBackend.getCurrentRole();
|
||||
const userName = HybridBackend.getCurrentUserName();
|
||||
const userEmail = HybridBackend.getCurrentUser();
|
||||
const activeWorkspace = HybridBackend.getActiveWorkspace();
|
||||
const clinColor = activeWorkspace?.cor || '#2563eb';
|
||||
|
||||
const fetchGoogleStatus = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/auth/google/status');
|
||||
const data = await response.json();
|
||||
setConnectedAccounts(data);
|
||||
} catch (error) {
|
||||
console.error('Erro ao buscar status do Google:', error);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchGoogleStatus();
|
||||
}, []);
|
||||
|
||||
const handleLogout = () => {
|
||||
if (window.confirm('TEM CERTEZA QUE DESEJA SAIR DO SISTEMA?')) {
|
||||
HybridBackend.logout();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-8 max-w-2xl">
|
||||
<div>
|
||||
<h1 className="text-2xl font-black text-gray-900 uppercase tracking-tight">Configurações</h1>
|
||||
<p className="text-sm text-gray-400 font-medium mt-1">Gerencie seu perfil e integrações</p>
|
||||
</div>
|
||||
|
||||
{/* Perfil */}
|
||||
<div className="bg-white rounded-2xl border border-gray-100 shadow-sm overflow-hidden">
|
||||
<div className="px-6 py-4 border-b border-gray-50">
|
||||
<h2 className="text-xs font-black text-gray-500 uppercase tracking-widest">Perfil</h2>
|
||||
</div>
|
||||
<div className="p-6">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-14 h-14 bg-gray-100 rounded-full flex items-center justify-center text-gray-500 border border-gray-200 flex-shrink-0">
|
||||
<User size={24} />
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<p className="text-base font-black text-gray-800 uppercase truncate leading-none mb-1">{userName || '—'}</p>
|
||||
<p className="text-sm font-medium text-gray-400 truncate">{userEmail || '—'}</p>
|
||||
<span
|
||||
className="inline-block mt-2 text-[10px] font-black uppercase tracking-widest px-2 py-0.5 rounded-full"
|
||||
style={{ backgroundColor: `${clinColor}18`, color: clinColor }}
|
||||
>
|
||||
{currentRole}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Unidade ativa */}
|
||||
<div className="bg-white rounded-2xl border border-gray-100 shadow-sm overflow-hidden">
|
||||
<div className="px-6 py-4 border-b border-gray-50">
|
||||
<h2 className="text-xs font-black text-gray-500 uppercase tracking-widest">Unidade Ativa</h2>
|
||||
</div>
|
||||
<div className="p-6">
|
||||
<div className="flex items-center gap-4">
|
||||
<div
|
||||
className="w-12 h-12 rounded-xl flex items-center justify-center text-white flex-shrink-0"
|
||||
style={{ backgroundColor: clinColor }}
|
||||
>
|
||||
<Building2 size={20} />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-black text-gray-800 uppercase">{activeWorkspace?.nome || 'Sem unidade vinculada'}</p>
|
||||
{activeWorkspace?.id && (
|
||||
<p className="text-[11px] text-gray-400 font-mono mt-0.5">ID: {activeWorkspace.id}</p>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className="ml-auto w-3 h-3 rounded-full flex-shrink-0"
|
||||
style={{ backgroundColor: clinColor }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Integrações */}
|
||||
{currentRole === 'admin' && (
|
||||
<div className="bg-white rounded-2xl border border-gray-100 shadow-sm overflow-hidden">
|
||||
<div className="px-6 py-4 border-b border-gray-50">
|
||||
<h2 className="text-xs font-black text-gray-500 uppercase tracking-widest">Integrações</h2>
|
||||
</div>
|
||||
<div className="p-6">
|
||||
<GoogleConnectButton
|
||||
ownerId="admin"
|
||||
isConnected={connectedAccounts.some(a => a.owner_id === 'admin' || a.owner_id === userEmail)}
|
||||
onStatusChange={fetchGoogleStatus}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Segurança */}
|
||||
<div className="bg-white rounded-2xl border border-gray-100 shadow-sm overflow-hidden">
|
||||
<div className="px-6 py-4 border-b border-gray-50">
|
||||
<h2 className="text-xs font-black text-gray-500 uppercase tracking-widest">Segurança</h2>
|
||||
</div>
|
||||
<div className="p-6 space-y-3">
|
||||
<div className="flex items-center gap-3 text-green-600 bg-green-50 px-4 py-3 rounded-xl border border-green-100">
|
||||
<ShieldCheck size={16} />
|
||||
<span className="text-xs font-bold uppercase tracking-wide">Sessão ativa e autenticada</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="w-full flex items-center justify-center gap-2 px-4 py-3 bg-red-50 text-red-600 hover:bg-red-100 rounded-xl text-xs font-black uppercase tracking-widest transition-all border border-red-100"
|
||||
>
|
||||
<LogOut size={16} />
|
||||
Sair do Sistema
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user