Files
scoreodonto.com/frontend/components/Sidebar.tsx
T
VPS 4 Builder ca7eba4531 feat(plugins): Salas como add-on por conta (pessoa-cêntrico) + Visualizar como biomédico
Superadmin / Visualizar como:
- adiciona BIOMÉDICO(A) à lista de preview do superadmin
- enterPreview ativa os plugins (RX/Salas/Profissionais) p/ teste; exitPreview restaura

Locação de Salas — ativação POR USUÁRIO (independe de clínica):
- backend: tabela usuario_plugins + GET/POST /api/me/plugins (authGuard, sem tenant)
- sala continua dona=owner_usuario_id (pessoa) + salaID, clinica_id opcional
- backend.ts: getMyPlugins/setMyPlugin/hydrateMyPlugins; login + App montam o hydrate
- isViewAllowed: 'salas' liberado se plugin ativo na conta; catálogo 'plugins' p/ não-paciente
- Sidebar: entrada ADD-ONS/PLUGINS p/ papéis operacionais ativarem
- PluginsView: toggle de locacao-salas persiste no servidor
- escopo: só locacao-salas é server-backed (RX/Profissionais seguem localStorage)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 15:04:25 +02:00

235 lines
12 KiB
TypeScript

import React from 'react';
import {
Users, Calendar as CalendarIcon, LayoutDashboard,
DollarSign, RefreshCw, Activity, BookOpen, Award, Megaphone, LogOut, ClipboardList, Bell, Building2, User, BarChart3, Settings, FileText, Puzzle, ScanLine, Monitor, UsersRound, GraduationCap, UserSearch, Briefcase, TrendingUp, Eye, FlaskConical, DoorOpen, Sparkles
} from 'lucide-react';
import { ToothIcon } from './ToothIcon.tsx';
import { HybridBackend } from '../services/backend.ts';
import { getActivePlugins } from '../views/plugins/pluginRegistry.ts';
interface SidebarProps {
activeTab: string;
setActiveTab: (t: string) => void;
}
export const Sidebar: React.FC<SidebarProps> = ({ activeTab, setActiveTab }) => {
const currentRole = HybridBackend.getCurrentRole();
const userName = HybridBackend.getCurrentUserName();
const userEmail = HybridBackend.getCurrentUser();
const activeWorkspace = HybridBackend.getActiveWorkspace();
const clinColor = activeWorkspace?.cor || (currentRole === 'superadmin' ? '#dc2626' : '#2563eb');
const activePluginIds = getActivePlugins();
const isTutor = (() => {
try { return JSON.parse(localStorage.getItem('SCOREODONTO_USER_DATA') || '{}').is_tutor === true; } catch { return false; }
})();
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') {
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).
if (['admin', 'donoclinica', 'donoconsultorio', 'superadmin'].includes(currentRole)) {
pluginMenuItems.push({ id: 'rx-config', label: 'DISPOSITIVOS RX', icon: Monitor, color: '#7c3aed' });
}
}
// Locação de Salas: todos os papéis operacionais (locador ou locatário), não o superadmin.
if (activePluginIds.includes('locacao-salas') && currentRole !== 'superadmin' && currentRole !== 'paciente') {
pluginMenuItems.push({ id: 'salas', label: 'LOCAÇÃO DE SALAS', icon: DoorOpen, color: '#0d9488' });
}
// Marketplace de Profissionais: clínicas contratam, profissionais divulgam perfil/recebem propostas.
if (activePluginIds.includes('profissionais-marketplace') && currentRole !== 'superadmin' && currentRole !== 'paciente') {
pluginMenuItems.push({ id: 'marketplace-profissionais', label: 'PROFISSIONAIS', icon: UserSearch, color: '#ea580c' });
}
// Catálogo de add-ons (por conta): qualquer usuário operacional pode abrir p/ ativar (ex.: Salas).
if (currentRole !== 'superadmin' && currentRole !== 'paciente') {
pluginMenuItems.push({ id: 'plugins', label: 'ADD-ONS / PLUGINS', icon: Puzzle, color: '#64748b' });
}
const menuItems = [
{ id: 'superadmin-overview', label: 'VISÃO GERAL', icon: TrendingUp },
{ id: 'superadmin-users', label: 'USUÁRIOS', icon: Users },
{ id: 'superadmin-clinicas', label: 'CLÍNICAS', icon: Building2 },
{ id: 'superadmin-logs', label: 'ACESSOS', icon: Activity },
{ id: 'superadmin-financeiro', label: 'FINANCEIRO', icon: DollarSign },
{ id: 'superadmin-notificacoes', label: 'NOTIFICAÇÕES', icon: Bell },
{ id: 'dashboard', label: 'VISÃO GERAL', icon: LayoutDashboard },
{ id: 'agenda', label: 'AGENDA', icon: CalendarIcon },
{ id: 'pacientes', label: 'PACIENTES', icon: Users },
{ id: 'lancar-gto', label: 'LANÇAR GTO', icon: ClipboardList },
{ id: 'ortodontia', label: 'ORTODONTIA', icon: Activity },
{ id: 'financeiro', label: 'FINANCEIRO', icon: DollarSign },
{ id: 'relatorios', label: 'RELATÓRIOS', icon: BarChart3 },
{ id: 'contratos', label: 'CONTRATOS', icon: FileText },
{ id: 'leads', label: 'GESTÃO DE LEADS', icon: Megaphone },
{ id: 'tratamentos', label: 'MEUS TRATAMENTOS', icon: ClipboardList },
{ id: 'clinicas', label: 'MINHAS CLÍNICAS', icon: Building2 },
{ id: 'dentistas', label: 'DENTISTAS', icon: ToothIcon },
{ id: 'especialidades', label: 'ESPECIALIDADES', icon: Award },
{ id: 'procedimentos', label: 'PROCEDIMENTOS', icon: ClipboardList },
{ id: 'planos', label: 'PLANOS / CONVÊNIOS', icon: BookOpen },
{ id: 'gestao-equipe', label: 'EQUIPE / USUÁRIOS', icon: UsersRound },
{ id: 'diretorio-profissionais', label: 'DIRETÓRIO PROF.', icon: UserSearch },
{ id: 'admin-gestao-tutores', label: 'GESTÃO TUTORES', icon: GraduationCap },
{ id: 'candidatura-tutor', label: 'SER TUTOR ORTO', icon: GraduationCap },
{ id: 'tutoria-painel', label: 'PAINEL TUTOR', icon: GraduationCap },
{ id: 'notificacoes', label: 'NOTIFICAÇÕES', icon: Bell },
{ id: 'plugins', label: 'PLUGINS', icon: Puzzle },
{ id: 'configuracoes', label: 'CONFIGURAÇÕES', icon: Settings },
].filter(item => {
// superadmin: itens específicos de administração global
if (currentRole === 'superadmin') return ['superadmin-overview', 'superadmin-users', 'superadmin-clinicas', 'superadmin-logs', 'superadmin-financeiro', 'superadmin-notificacoes', 'plugins', 'configuracoes'].includes(item.id);
// telas de superadmin são exclusivas do superadmin (não vazam p/ admin/donos)
if (item.id.startsWith('superadmin')) return false;
// plugins e gestão de tutores: exclusivo superadmin
if (item.id === 'plugins' || item.id === 'admin-gestao-tutores') return false;
// diretório antigo é absorvido pelo plugin Profissionais quando ele está ativo
if (item.id === 'diretorio-profissionais' && activePluginIds.includes('profissionais-marketplace')) return false;
// painel tutor: só para tutores aprovados
if (item.id === 'tutoria-painel') return isTutor;
// RBAC por CARGO: membro não-dono com permissões definidas no workspace ativo
const wsRole = (activeWorkspace as any)?.role;
const wsPerms = (activeWorkspace as any)?.permissoes;
const isWsOwner = ['admin', 'donoclinica', 'donoconsultorio'].includes(wsRole || '');
if (!isWsOwner && Array.isArray(wsPerms) && wsPerms.length > 0) {
// Navegação/conta sempre visível
if (['clinicas', 'configuracoes', 'notificacoes'].includes(item.id)) return true;
return wsPerms.includes(item.id);
}
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);
if (currentRole === 'funcionario') return ['dashboard', 'leads', 'pacientes', 'tratamentos', 'lancar-gto', 'agenda', 'financeiro', 'relatorios', 'notificacoes', 'clinicas', 'configuracoes', 'contratos', 'diretorio-profissionais'].includes(item.id);
return false;
});
const handleLogout = () => {
if (window.confirm('TEM CERTEZA QUE DESEJA SAIR DO SISTEMA?')) {
HybridBackend.logout();
}
};
const workspaceName = activeWorkspace?.nome || 'SCOREODONTO';
const WorkspaceIcon = currentRole === 'donoconsultorio' ? Briefcase : Building2;
return (
<div className="w-64 bg-white border-r border-gray-200 h-screen flex flex-col shadow-xl md:shadow-none">
<div className="p-6 flex items-center gap-3">
<div
className="w-8 h-8 rounded-lg flex items-center justify-center text-white font-bold shadow-lg transition-colors duration-500"
style={{ backgroundColor: clinColor }}
>
<WorkspaceIcon size={16} />
</div>
<div className="min-w-0 flex-1">
<span className="font-bold text-sm text-gray-800 block leading-tight tracking-tight truncate uppercase">{workspaceName}</span>
<span className="text-[9px] text-gray-400 font-bold uppercase tracking-wider">SCOREODONTO</span>
</div>
</div>
<nav className="flex-1 px-4 py-2 space-y-1 overflow-y-auto custom-scrollbar">
{menuItems.map((item) => {
const Icon = item.icon;
const isActive = activeTab === item.id;
return (
<button
key={item.id}
onClick={() => setActiveTab(item.id)}
className={`w-full flex items-center gap-3 px-4 py-3 rounded-xl text-[11px] font-bold uppercase transition-all duration-300 group ${isActive
? 'text-white'
: 'text-gray-500 hover:bg-gray-50'
}`}
style={{
backgroundColor: isActive ? clinColor : 'transparent',
boxShadow: isActive ? `0 10px 15px -5px ${clinColor}44` : 'none'
}}
>
<Icon size={18} className={`${isActive ? 'text-white' : 'text-gray-400 group-hover:text-gray-600'} transition-colors`}
style={{ color: isActive ? '#fff' : undefined }} />
{item.label}
</button>
);
})}
{currentRole === 'superadmin' && (
<div className="pt-3 mt-2 border-t border-gray-100">
<p className="px-4 pb-1 text-[9px] font-black text-gray-400 uppercase tracking-widest flex items-center gap-1.5">
<Eye size={11} /> Visualizar como
</p>
{[
{ role: 'donoclinica', label: 'DONO DE CLÍNICA', icon: Building2 },
{ role: 'dentista', label: 'DENTISTA', icon: ToothIcon },
{ role: 'biomedico', label: 'BIOMÉDICO(A)', icon: Sparkles },
{ role: 'protetico', label: 'PROTÉTICO', icon: FlaskConical },
{ role: 'paciente', label: 'PACIENTE', icon: User },
].map(({ role, label, icon: Icon }) => (
<button
key={role}
onClick={() => HybridBackend.enterPreview(role)}
className="w-full flex items-center gap-3 px-4 py-2.5 rounded-xl text-[11px] font-bold uppercase text-gray-500 hover:bg-gray-50 transition-all group"
>
<Icon size={18} className="text-gray-400 group-hover:text-gray-600 transition-colors" />
{label}
</button>
))}
</div>
)}
{pluginMenuItems.map((item) => {
const Icon = item.icon;
const isActive = activeTab === item.id;
return (
<button
key={item.id}
onClick={() => setActiveTab(item.id)}
className={`w-full flex items-center gap-3 px-4 py-3 rounded-xl text-[11px] font-bold uppercase transition-all duration-300 group ${isActive ? 'text-white' : 'text-gray-500 hover:bg-purple-50'}`}
style={{
backgroundColor: isActive ? item.color : 'transparent',
boxShadow: isActive ? `0 10px 15px -5px ${item.color}44` : 'none'
}}
>
<Icon size={18} className={isActive ? 'text-white' : ''} style={{ color: isActive ? '#fff' : item.color }} />
{item.label}
</button>
);
})}
</nav>
<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="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>
<Settings size={14} className={activeTab === 'configuracoes' ? 'text-white' : 'text-gray-400'} />
</button>
<button
onClick={handleLogout}
className="w-full flex items-center gap-3 px-4 py-2 text-red-500 hover:bg-red-50 rounded-xl text-[10px] font-black uppercase transition-all"
>
<LogOut size={16} />
SAIR DO SISTEMA
</button>
</div>
</div>
);
};