Merge branch 'claude/youthful-mendel-0c569f'

This commit is contained in:
VPS 4 Builder
2026-05-14 04:54:25 +02:00
3 changed files with 159 additions and 57 deletions
+10 -5
View File
@@ -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={() => {
+20 -52
View File
@@ -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}
+129
View File
@@ -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>
);
};