Oleada B: fusion Expedientes en Pacientes (+chip sin visita 6m), sidebar colapsable SKEEN, edicion completa de citas, autocompletado de paciente, bloqueos excluidos de slots
This commit is contained in:
@@ -26,13 +26,13 @@ import {
|
||||
ClipboardPlus,
|
||||
Banknote,
|
||||
Percent,
|
||||
FolderClock,
|
||||
Star,
|
||||
Pencil,
|
||||
ChevronUp,
|
||||
ChevronDown,
|
||||
MessageCircle,
|
||||
CircleDollarSign,
|
||||
PanelsTopLeft,
|
||||
} from 'lucide-react';
|
||||
import { Button, Modal, Input } from './ui';
|
||||
import QuickLinksDock, { QuickLinksModePicker } from './QuickLinksDock';
|
||||
@@ -66,7 +66,6 @@ const menuGroups = [
|
||||
{ label: 'Pacientes', icon: Users, href: '/pacientes' },
|
||||
{ label: 'Visitas', icon: ClipboardList, href: '/visitas' },
|
||||
{ label: 'Consultas', icon: ClipboardPlus, href: '/consultas' },
|
||||
{ label: 'Expedientes', icon: FolderClock, href: '/expedientes' },
|
||||
{ label: 'Mis ventas', icon: CircleDollarSign, href: '/mis-ventas' },
|
||||
{ label: 'Médicos', icon: Stethoscope, href: '/medicos' },
|
||||
{ label: 'Servicios', icon: Briefcase, href: '/servicios' },
|
||||
@@ -114,6 +113,19 @@ export const SkeenLayout: FC<LayoutProps> = ({ children, title, subtitle }) => {
|
||||
const { user, canSee, hasRole, logout } = useAuth();
|
||||
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||||
const chatUnread = useChatUnread();
|
||||
// Sidebar colapsable (modo iconos). Preferencia persistente; en pantallas
|
||||
// medianas (menores a 1440px) el estado inicial es colapsado.
|
||||
const [collapsed, setCollapsed] = useState<boolean>(() => {
|
||||
const saved = localStorage.getItem('skeen_sidebar_collapsed');
|
||||
if (saved !== null) return saved === '1';
|
||||
return window.innerWidth < 1440;
|
||||
});
|
||||
const toggleCollapsed = () => {
|
||||
setCollapsed((v) => {
|
||||
localStorage.setItem('skeen_sidebar_collapsed', v ? '0' : '1');
|
||||
return !v;
|
||||
});
|
||||
};
|
||||
|
||||
const visibleGroups = useMemo(
|
||||
() =>
|
||||
@@ -215,14 +227,51 @@ export const SkeenLayout: FC<LayoutProps> = ({ children, title, subtitle }) => {
|
||||
return location.pathname === href || location.pathname.startsWith(`${href}/`);
|
||||
};
|
||||
|
||||
const SidebarContent = () => (
|
||||
const SidebarContent = ({ collapsed = false, showToggle = true }: { collapsed?: boolean; showToggle?: boolean }) => (
|
||||
<>
|
||||
<div className="h-20 flex items-center px-5 lg:px-6 border-b border-white/10 shrink-0">
|
||||
<img src={LOGO_SKEEN} alt="SKEEN" className="h-7 w-auto object-contain" />
|
||||
<div className={`h-20 flex items-center border-b border-white/10 shrink-0 ${collapsed ? 'justify-center px-2' : 'px-5 lg:px-6'}`}>
|
||||
<img src={LOGO_SKEEN} alt="SKEEN" className={collapsed ? 'h-6 w-auto object-contain' : 'h-7 w-auto object-contain'} />
|
||||
{!collapsed && showToggle && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleCollapsed}
|
||||
title="Colapsar menú"
|
||||
aria-label="Colapsar menú"
|
||||
className="ml-auto p-1.5 rounded-lg text-white/40 hover:text-white hover:bg-theme-surface/10 transition-colors"
|
||||
>
|
||||
<PanelsTopLeft size={16} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<nav className="flex-1 overflow-y-auto py-5 px-3">
|
||||
{quickItems.length > 0 && quick.mode === 'menu' && (
|
||||
{quickItems.length > 0 && quick.mode === 'menu' && collapsed && (
|
||||
<div className="mb-5" title="Accesos rápidos">
|
||||
<div className="mx-3 mb-2 border-t border-white/10" />
|
||||
<ul className="space-y-1 flex flex-col items-center">
|
||||
{quickItems.map((item) => {
|
||||
const Icon = item.icon;
|
||||
const active = isActive(item.href);
|
||||
return (
|
||||
<li key={item.href}>
|
||||
<Link
|
||||
to={item.href}
|
||||
onClick={() => setSidebarOpen(false)}
|
||||
title={item.label}
|
||||
aria-label={item.label}
|
||||
className={`flex items-center justify-center w-10 h-10 rounded-xl transition-all ${
|
||||
active ? 'bg-theme-accent text-theme-inverse' : 'text-white/70 hover:bg-theme-surface/10 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
<Icon size={18} />
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
{quickItems.length > 0 && quick.mode === 'menu' && !collapsed && (
|
||||
<div className="mb-6">
|
||||
<div className="flex items-center justify-between px-3 mb-2">
|
||||
<p className="text-[10px] uppercase tracking-wider text-white/40 font-heading">Accesos rápidos</p>
|
||||
@@ -280,42 +329,54 @@ export const SkeenLayout: FC<LayoutProps> = ({ children, title, subtitle }) => {
|
||||
)}
|
||||
{visibleGroups.map((group) => (
|
||||
<div key={group.label} className="mb-6">
|
||||
<p className="px-3 text-[10px] uppercase tracking-wider text-white/40 mb-2 font-heading">{group.label}</p>
|
||||
{collapsed ? (
|
||||
<div className="mx-3 mb-2 border-t border-white/10" title={group.label} />
|
||||
) : (
|
||||
<p className="px-3 text-[10px] uppercase tracking-wider text-white/40 mb-2 font-heading">{group.label}</p>
|
||||
)}
|
||||
<ul className="space-y-1">
|
||||
{group.items.map((item) => {
|
||||
const Icon = item.icon;
|
||||
const active = isActive(item.href);
|
||||
const pinned = quick.isPinned(item.href);
|
||||
return (
|
||||
<li key={item.label} className="group flex items-center gap-1">
|
||||
<li key={item.label} className={`group flex items-center ${collapsed ? 'justify-center' : 'gap-1'}`}>
|
||||
<Link
|
||||
to={item.href}
|
||||
onClick={() => setSidebarOpen(false)}
|
||||
className={`flex-1 flex items-center space-x-3 px-3 py-2.5 rounded-xl text-sm font-medium transition-all ${
|
||||
title={item.label}
|
||||
aria-label={item.label}
|
||||
className={`${collapsed ? 'relative flex items-center justify-center w-10 h-10 rounded-xl' : 'flex-1 flex items-center space-x-3 px-3 py-2.5 rounded-xl'} text-sm font-medium transition-all ${
|
||||
active
|
||||
? 'bg-theme-accent text-theme-inverse shadow-soft'
|
||||
: 'text-white/70 hover:bg-theme-surface/10 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
<Icon size={18} />
|
||||
<span>{item.label}</span>
|
||||
{!collapsed && <span>{item.label}</span>}
|
||||
{item.href === '/chat' && chatUnread > 0 && (
|
||||
<span className="ml-auto inline-flex items-center justify-center min-w-[18px] h-[18px] px-1 rounded-full bg-brand-coral text-white text-[10px] font-bold">
|
||||
{chatUnread}
|
||||
</span>
|
||||
collapsed ? (
|
||||
<span className="absolute top-1.5 right-1.5 w-2 h-2 rounded-full bg-brand-coral" title={`${chatUnread} sin leer`} />
|
||||
) : (
|
||||
<span className="ml-auto inline-flex items-center justify-center min-w-[18px] h-[18px] px-1 rounded-full bg-brand-coral text-white text-[10px] font-bold">
|
||||
{chatUnread}
|
||||
</span>
|
||||
)
|
||||
)}
|
||||
{active && <ChevronRight size={14} className={item.href === '/chat' && chatUnread > 0 ? '' : 'ml-auto'} />}
|
||||
{active && !collapsed && <ChevronRight size={14} className={item.href === '/chat' && chatUnread > 0 ? '' : 'ml-auto'} />}
|
||||
</Link>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => quick.toggle(item.href)}
|
||||
className={`shrink-0 transition-all ${
|
||||
pinned ? 'text-amber-400' : 'text-white/30 opacity-0 group-hover:opacity-100 hover:text-amber-400'
|
||||
}`}
|
||||
title={pinned ? 'Quitar de accesos rápidos' : 'Fijar en accesos rápidos'}
|
||||
>
|
||||
<Star size={15} fill={pinned ? 'currentColor' : 'none'} />
|
||||
</button>
|
||||
{!collapsed && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => quick.toggle(item.href)}
|
||||
className={`shrink-0 transition-all ${
|
||||
pinned ? 'text-amber-400' : 'text-white/30 opacity-0 group-hover:opacity-100 hover:text-amber-400'
|
||||
}`}
|
||||
title={pinned ? 'Quitar de accesos rápidos' : 'Fijar en accesos rápidos'}
|
||||
>
|
||||
<Star size={15} fill={pinned ? 'currentColor' : 'none'} />
|
||||
</button>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
@@ -324,15 +385,17 @@ export const SkeenLayout: FC<LayoutProps> = ({ children, title, subtitle }) => {
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<div className="p-4 border-t border-white/10 shrink-0">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="w-10 h-10 rounded-full bg-theme-accent/20 flex items-center justify-center text-sm font-bold text-theme-accent">
|
||||
<div className={`border-t border-white/10 shrink-0 ${collapsed ? 'p-2 flex flex-col items-center gap-2' : 'p-4'}`}>
|
||||
<div className={`flex items-center ${collapsed ? 'flex-col gap-2' : 'space-x-3'}`}>
|
||||
<div className="w-10 h-10 rounded-full bg-theme-accent/20 flex items-center justify-center text-sm font-bold text-theme-accent" title={user?.name || 'Usuario'}>
|
||||
{initials || 'US'}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-white truncate">{user?.name || 'Usuario'}</p>
|
||||
<p className="text-xs text-white/50 truncate">{user ? ROLE_LABELS[user.role] : ''}</p>
|
||||
</div>
|
||||
{!collapsed && (
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-white truncate">{user?.name || 'Usuario'}</p>
|
||||
<p className="text-xs text-white/50 truncate">{user ? ROLE_LABELS[user.role] : ''}</p>
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="p-2 rounded-lg text-white/50 hover:text-white hover:bg-theme-surface/10"
|
||||
@@ -342,14 +405,28 @@ export const SkeenLayout: FC<LayoutProps> = ({ children, title, subtitle }) => {
|
||||
<LogOut size={18} />
|
||||
</button>
|
||||
</div>
|
||||
{collapsed && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleCollapsed}
|
||||
title="Expandir menú"
|
||||
aria-label="Expandir menú"
|
||||
className="p-1.5 rounded-lg text-white/40 hover:text-white hover:bg-theme-surface/10 transition-colors"
|
||||
>
|
||||
<PanelsTopLeft size={16} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex" style={{ backgroundColor: 'var(--bg)', color: 'var(--text)' }}>
|
||||
<aside className="hidden lg:flex flex-col w-64 fixed h-full z-20" style={{ backgroundColor: 'var(--sidebar-bg)' }}>
|
||||
<SidebarContent />
|
||||
<aside
|
||||
className={`hidden lg:flex flex-col fixed h-full z-20 transition-all duration-200 ${collapsed ? 'w-16' : 'w-64'}`}
|
||||
style={{ backgroundColor: 'var(--sidebar-bg)' }}
|
||||
>
|
||||
<SidebarContent collapsed={collapsed} />
|
||||
</aside>
|
||||
|
||||
{sidebarOpen && (
|
||||
@@ -371,7 +448,7 @@ export const SkeenLayout: FC<LayoutProps> = ({ children, title, subtitle }) => {
|
||||
<SidebarContent />
|
||||
</aside>
|
||||
|
||||
<div className="flex-1 lg:ml-64 min-h-screen flex flex-col w-full">
|
||||
<div className={`flex-1 min-h-screen flex flex-col w-full transition-all duration-200 ${collapsed ? 'lg:ml-16' : 'lg:ml-64'}`}>
|
||||
<header className="backdrop-blur border-b sticky top-0 z-10" style={{ backgroundColor: 'var(--header-bg)', borderColor: 'var(--border)' }}>
|
||||
<div className="px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex items-center justify-between h-16">
|
||||
|
||||
Reference in New Issue
Block a user