21bebd3256
- Drawer de WhatsApp focado (WhatsChatDrawer) aberto do slot do agendamento, com dropdown de números do paciente + grupo familiar e fluxo de iniciar conversa - Auto-provisionamento de conta no motor: eager no cadastro/convite + self-heal no proxy (404 "conta não encontrada" → provisiona e refaz o request) - Ownership por sessão (wa_session_authz): dono do workspace (owner_id ou vínculo donoclinica/donoconsultorio) tem poder total; delega re-scan, excluir sessão, apagar conversa/mensagem, acesso ao Inbox e à Secretária — por conta - Enforcement no proxy: guardSessionAction (scan/rescan/delete sessão), guardAreaAccess (inbox/secretaria), guardInboxDestructive (apagar conversa/msg) - Endpoints /api/nw/authz (dono gerencia) e /api/nw/access (acesso agregado) - Assinatura do operador nas mensagens (*Nome*, desambiguação de homônimos → *Rui C.*) - UI: painel "Gerenciar acesso" com 5 toggles; menu e botões de apagar condicionados - Proxy de mídia /api/nw/media + re-download sob demanda Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
339 lines
21 KiB
TypeScript
339 lines
21 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import { useConfirm } from '../contexts/ConfirmContext.tsx';
|
|
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, FileSignature, Percent, ShieldAlert, ChevronDown, MessageCircle, Bot, Smartphone
|
|
} from 'lucide-react';
|
|
import { ToothIcon } from './ToothIcon.tsx';
|
|
import { HybridBackend } from '../services/backend.ts';
|
|
import { getActivePlugins, isPluginActive } 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; }
|
|
})();
|
|
|
|
// Acesso do usuário às áreas do WhatsApp (Inbox/Secretária). null = ainda carregando
|
|
// (fail-open: mostra até saber). Dono e autorizados veem; os demais não.
|
|
const [waAccess, setWaAccess] = useState<{ isOwner: boolean; inbox: boolean; secretaria: boolean; delete_msg: boolean } | null>(null);
|
|
useEffect(() => {
|
|
let alive = true;
|
|
if (isPluginActive('newwhats')) {
|
|
HybridBackend.getWaAccess().then(a => { if (alive) setWaAccess(a); }).catch(() => {});
|
|
}
|
|
return () => { alive = false; };
|
|
}, [activeWorkspace?.id]);
|
|
const canSeeInbox = !waAccess || waAccess.isOwner || waAccess.inbox;
|
|
const canSeeSecretaria = !waAccess || waAccess.isOwner || waAccess.secretaria;
|
|
|
|
const pluginMenuItems: Array<{ id: string; label: string; icon: any; color: string }> = [];
|
|
if (activePluginIds.includes('rx-scoreodonto')) {
|
|
// 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).
|
|
if (['admin', 'donoclinica', 'donoconsultorio', 'superadmin'].includes(currentRole)) {
|
|
pluginMenuItems.push({ id: 'rx-config', label: 'DISPOSITIVOS RX', icon: Monitor, color: '#7c3aed' });
|
|
}
|
|
}
|
|
// NewWhats: Inbox + Secretária IA — para TODOS os usuários quando o plugin está
|
|
// ativo (a CONFIG do plugin fica no catálogo PLUGINS, exclusivo do superadmin).
|
|
if (isPluginActive('newwhats')) {
|
|
if (canSeeInbox) pluginMenuItems.push({ id: 'wa-inbox', label: 'WHATSAPP', icon: MessageCircle, color: '#16a34a' });
|
|
// INSTÂNCIAS (gestão/QR) só para o dono; os demais operam via Inbox.
|
|
if (!waAccess || waAccess.isOwner) pluginMenuItems.push({ id: 'wa-sessions', label: 'INSTÂNCIAS', icon: Smartphone, color: '#0ea5e9' });
|
|
if (canSeeSecretaria) pluginMenuItems.push({ id: 'wa-secretaria', label: 'SECRETÁRIA IA', icon: Bot, color: '#2563eb' });
|
|
}
|
|
// Locação de Salas: menus de LOCADOR (administra as próprias salas) e LOCATÁRIO (aluga de
|
|
// terceiros). São decisões de NEGÓCIO do titular — não cabem a funcionário (subordinado) nem
|
|
// a paciente. Funcionário, quando muito, opera a AGENDA da sala (workspace tipo='sala'), não
|
|
// a locação. Por isso restringimos a papéis que podem ser titulares de sala.
|
|
const PAPEIS_LOCACAO = ['donosala', 'donoclinica', 'donoconsultorio', 'admin', 'dentista', 'biomedico', 'protetico'];
|
|
if (PAPEIS_LOCACAO.includes(currentRole)) {
|
|
pluginMenuItems.push({ id: 'minhas-salas', label: 'MINHAS SALAS', icon: DoorOpen, color: '#0d9488' });
|
|
pluginMenuItems.push({ id: 'alugar-salas', label: 'ALUGAR SALAS', icon: Building2, color: '#0d9488' });
|
|
}
|
|
// Marketplace de Profissionais: clínicas contratam, profissionais divulgam perfil/recebem propostas.
|
|
// O protético DEPENDE desta tela para publicar o perfil no diretório e ser encontrado pelas clínicas
|
|
// — por isso aparece sempre para ele, mesmo sem o plugin ativado.
|
|
if ((activePluginIds.includes('profissionais-marketplace') || currentRole === 'protetico' || currentRole === 'biomedico' || currentRole === 'dentista') && currentRole !== 'superadmin' && currentRole !== 'paciente') {
|
|
pluginMenuItems.push({ id: 'marketplace-profissionais', label: 'PROFISSIONAIS', icon: UserSearch, color: '#0d9488' });
|
|
}
|
|
// OBS: o catálogo de PLUGINS é EXCLUSIVO do superadmin (gerência/ativação).
|
|
// Não exibir para nenhum outro papel.
|
|
|
|
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: 'sala-home', label: 'PAINEL DA SALA', icon: DoorOpen },
|
|
{ id: 'lab-home', label: 'PAINEL DO LAB', icon: LayoutDashboard },
|
|
{ id: 'lab-bancada', label: 'BANCADA', icon: FlaskConical },
|
|
{ id: 'lab-agenda', label: 'AGENDA', icon: CalendarIcon },
|
|
{ id: 'lab-clientes', label: 'CLIENTES', icon: Building2 },
|
|
{ id: 'lab-cotacoes', label: 'COTAÇÕES', icon: FileText },
|
|
{ id: 'lab-financeiro', label: 'FINANCEIRO', icon: DollarSign },
|
|
{ id: 'lab-indicadores', label: 'INDICADORES', icon: BarChart3 },
|
|
{ id: 'lab-equipe', label: 'EQUIPE', icon: UsersRound },
|
|
{ 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: 'proteses', label: 'PRÓTESES', icon: FlaskConical },
|
|
{ id: 'ortodontia', label: 'ORTODONTIA', icon: Activity },
|
|
{ id: 'financeiro', label: 'FINANCEIRO', icon: DollarSign },
|
|
{ id: 'relatorios', label: 'RELATÓRIOS', icon: BarChart3 },
|
|
{ id: 'comissoes', label: 'COMISSÕES', icon: Percent },
|
|
{ id: 'glosas', label: 'GLOSAS', icon: ShieldAlert },
|
|
{ id: 'orcamentos', label: 'ORÇAMENTOS', icon: FileText },
|
|
{ id: 'contratos', label: 'CONTRATOS', icon: FileSignature },
|
|
{ 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;
|
|
|
|
// PAINEL DA SALA só existe quando o workspace ativo é uma sala.
|
|
if (item.id === 'sala-home') return (activeWorkspace as any)?.tipo === 'sala';
|
|
// PAINEL DO LAB / BANCADA só existem no workspace de laboratório.
|
|
if (['lab-home', 'lab-bancada', 'lab-agenda', 'lab-clientes', 'lab-cotacoes', 'lab-financeiro', 'lab-equipe', 'lab-indicadores'].includes(item.id)) return (activeWorkspace as any)?.tipo === 'laboratorio';
|
|
|
|
// Contexto de SALA (workspace tipo='sala'): isola da clínica — menu base só conta/notificações.
|
|
// (MINHAS SALAS / ALUGAR SALAS são itens de plugin, mostrados à parte.)
|
|
if ((activeWorkspace as any)?.tipo === 'sala') return ['sala-home', 'notificacoes', 'configuracoes'].includes(item.id);
|
|
|
|
// Contexto de LABORATÓRIO (workspace tipo='laboratorio'): área própria do protético —
|
|
// painel do lab + bancada de produção, sem as telas clínicas da clínica.
|
|
if ((activeWorkspace as any)?.tipo === 'laboratorio') {
|
|
// Técnico (vínculo tecnico_protese) só opera a produção; gestão é do dono.
|
|
const ehTecnico = (activeWorkspace as any)?.role === 'tecnico_protese';
|
|
return ehTecnico
|
|
? ['lab-home', 'lab-bancada', 'notificacoes', 'configuracoes'].includes(item.id)
|
|
: ['lab-home', 'lab-bancada', 'lab-agenda', 'lab-clientes', 'lab-cotacoes', 'lab-financeiro', 'lab-indicadores', 'lab-equipe', 'notificacoes', 'configuracoes'].includes(item.id);
|
|
}
|
|
|
|
// 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;
|
|
// Dono de sala (locador não-clínico): só conta/notificações; salas vêm como itens de plugin.
|
|
if (currentRole === 'donosala') return ['notificacoes', 'configuracoes'].includes(item.id);
|
|
if (currentRole === 'paciente') return ['tratamentos', 'notificacoes', 'configuracoes'].includes(item.id);
|
|
// Dentista: clínico odonto completo (ortodontia, tutoria orto).
|
|
if (currentRole === 'dentista') return ['dashboard', 'tratamentos', 'agenda', 'ortodontia', 'proteses', 'notificacoes', 'clinicas', 'configuracoes', 'candidatura-tutor', 'diretorio-profissionais'].includes(item.id);
|
|
// Biomédico(a): seu próprio catálogo (especialidades/procedimentos de biomedicina);
|
|
// SEM itens odontológicos (ortodontia, RX, tutoria orto).
|
|
if (currentRole === 'biomedico') return ['dashboard', 'agenda', 'tratamentos', 'especialidades', 'procedimentos', 'notificacoes', 'clinicas', 'configuracoes', 'diretorio-profissionais'].includes(item.id);
|
|
// Protético: laboratório — agenda própria + GTO; sem ortodontia/RX/pacientes clínicos.
|
|
// Protético: opera dentro do workspace 'laboratorio' (menu governado pelo branch tipo acima).
|
|
// Este é só o fallback fora desse contexto — sem telas clínicas (agenda/GTO/tratamentos).
|
|
if (currentRole === 'protetico') return ['notificacoes', 'clinicas', 'configuracoes', 'diretorio-profissionais'].includes(item.id);
|
|
if (currentRole === 'funcionario') return ['dashboard', 'leads', 'pacientes', 'tratamentos', 'lancar-gto', 'proteses', 'agenda', 'financeiro', 'relatorios', 'glosas', 'notificacoes', 'clinicas', 'configuracoes', 'orcamentos', 'contratos', 'diretorio-profissionais'].includes(item.id);
|
|
return false;
|
|
});
|
|
|
|
const confirm = useConfirm();
|
|
const [wsOpen, setWsOpen] = React.useState(false);
|
|
|
|
const handleLogout = async () => {
|
|
if (await confirm('TEM CERTEZA QUE DESEJA SAIR DO SISTEMA?')) {
|
|
HybridBackend.logout();
|
|
}
|
|
};
|
|
|
|
const workspaceName = activeWorkspace?.nome || 'SCOREODONTO';
|
|
const activeIsSala = (activeWorkspace as any)?.tipo === 'sala';
|
|
const WorkspaceIcon = activeIsSala ? DoorOpen : currentRole === 'donoconsultorio' ? Briefcase : Building2;
|
|
// Workspaces para o seletor: clínicas (via vínculo) + salas próprias, agrupados.
|
|
const allWorkspaces: any[] = HybridBackend.getWorkspaces() || [];
|
|
const wsClinicas = allWorkspaces.filter(w => w.tipo !== 'sala');
|
|
const wsSalas = allWorkspaces.filter(w => w.tipo === 'sala');
|
|
const canSwitch = allWorkspaces.length > 1;
|
|
|
|
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 pb-3 relative">
|
|
<button type="button" onClick={() => canSwitch && setWsOpen(o => !o)} className={`w-full flex items-center gap-3 ${canSwitch ? 'hover:bg-gray-50 rounded-xl -m-1 p-1' : 'cursor-default'} transition-colors`}>
|
|
<div className="w-8 h-8 rounded-lg flex items-center justify-center text-white font-bold shadow-lg transition-colors duration-500 shrink-0" style={{ backgroundColor: clinColor }}>
|
|
<WorkspaceIcon size={16} />
|
|
</div>
|
|
<div className="min-w-0 flex-1 text-left">
|
|
<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">{activeIsSala ? 'SALA · LOCAÇÃO' : 'SCOREODONTO'}</span>
|
|
</div>
|
|
{canSwitch && <ChevronDown size={16} className={`text-gray-300 shrink-0 transition-transform ${wsOpen ? 'rotate-180' : ''}`} />}
|
|
</button>
|
|
{wsOpen && canSwitch && (
|
|
<>
|
|
<div className="fixed inset-0 z-10" onClick={() => setWsOpen(false)} />
|
|
<div className="absolute left-4 right-4 top-[72px] z-20 bg-white border border-gray-200 rounded-2xl shadow-xl py-2 max-h-[60vh] overflow-y-auto">
|
|
{wsClinicas.length > 0 && <p className="px-4 py-1.5 text-[9px] font-black text-gray-400 uppercase tracking-widest">Clínicas</p>}
|
|
{wsClinicas.map(w => (
|
|
<button key={w.id} onClick={() => { setWsOpen(false); HybridBackend.switchWorkspace(w.id); }}
|
|
className={`w-full flex items-center gap-2.5 px-4 py-2 hover:bg-gray-50 text-left ${activeWorkspace?.id === w.id ? 'bg-gray-50' : ''}`}>
|
|
<span className="w-6 h-6 rounded-md flex items-center justify-center text-white shrink-0" style={{ backgroundColor: w.cor || '#2563eb' }}><Building2 size={12} /></span>
|
|
<span className="text-xs font-bold text-gray-700 uppercase truncate flex-1">{w.nome}</span>
|
|
{activeWorkspace?.id === w.id && <span className="w-2 h-2 rounded-full bg-teal-500 shrink-0" />}
|
|
</button>
|
|
))}
|
|
{wsSalas.length > 0 && <p className="px-4 py-1.5 mt-1 text-[9px] font-black text-gray-400 uppercase tracking-widest border-t border-gray-50 pt-2">Salas</p>}
|
|
{wsSalas.map(w => (
|
|
<button key={w.id} onClick={() => { setWsOpen(false); HybridBackend.switchWorkspace(w.id); }}
|
|
className={`w-full flex items-center gap-2.5 px-4 py-2 hover:bg-gray-50 text-left ${activeWorkspace?.id === w.id ? 'bg-gray-50' : ''}`}>
|
|
<span className="w-6 h-6 rounded-md flex items-center justify-center text-white shrink-0" style={{ backgroundColor: '#0d9488' }}><DoorOpen size={12} /></span>
|
|
<span className="text-xs font-bold text-gray-700 uppercase truncate flex-1">{w.nome}{w.cidade ? <span className="text-gray-400 font-medium normal-case"> · {w.cidade}/{w.estado}</span> : ''}</span>
|
|
{activeWorkspace?.id === w.id && <span className="w-2 h-2 rounded-full bg-teal-500 shrink-0" />}
|
|
</button>
|
|
))}
|
|
</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>
|
|
);
|
|
};
|