Permisos: marketing (export completo, agenda/citas, cumpleanos, visitas, WACRM), gerencia gestiona usuarios y roles, recepcion edita expedientes, cosmetologas ven agenda, fix del sidebar que no dejaba seleccionar Leads

This commit is contained in:
2026-09-18 03:23:44 +00:00
parent c38783eb31
commit cebf8d663d
13 changed files with 354 additions and 47 deletions

View File

@@ -1,5 +1,5 @@
import type { FC, ReactNode } from 'react';
import { useEffect, useMemo, useState } from 'react';
import type { FC, MutableRefObject, ReactNode } from 'react';
import { useEffect, useMemo, useRef, useState } from 'react';
import { Link, useLocation, useNavigate } from 'react-router-dom';
import {
LayoutDashboard,
@@ -256,13 +256,47 @@ export const ClasicoLayout: FC<LayoutProps> = ({ children, title: _title, subtit
const toggleGroup = (label: string) =>
setOpenGroups((prev) => (prev.includes(label) ? prev.filter((l) => l !== label) : [...prev, label]));
const SidebarContent = () => (
// El menú se renderiza como función (no como componente) a propósito: como
// componente definido dentro del layout, cada render crea un tipo nuevo y
// React remonta el subárbol entero, perdiendo la posición de scroll (los
// últimos items, p. ej. Leads WACRM, quedaban inalcanzables).
const navDesktopRef = useRef<HTMLElement | null>(null);
const navMobileRef = useRef<HTMLElement | null>(null);
// 1) El grupo que contiene la ruta activa siempre queda abierto
useEffect(() => {
const current = `${location.pathname}${location.search}`;
const abiertos = menuTree
.filter((e) => e.children?.some((c) => {
const base = c.href.split('?')[0];
return current === c.href || (current.startsWith(base) && (!c.href.includes('?') || current.includes(c.href.split('?')[1])));
}))
.map((e) => e.label);
if (!abiertos.length) return;
setOpenGroups((prev) => {
const faltan = abiertos.filter((l) => !prev.includes(l));
return faltan.length ? [...prev, ...faltan] : prev;
});
}, [location.pathname, location.search]);
// 2) Con el grupo ya abierto (re-render), deja visible el item activo.
// Va en un efecto aparte a propósito: en el mismo tick el item todavía no
// está en el DOM y no habría nada que desplazar.
useEffect(() => {
for (const nav of [navDesktopRef.current, navMobileRef.current]) {
if (!nav || nav.scrollHeight <= nav.clientHeight) continue;
const el = nav.querySelector<HTMLElement>('[data-active="1"]');
if (el) el.scrollIntoView({ block: 'nearest' });
}
}, [location.pathname, location.search, openGroups, visibleLeaves]);
const renderSidebar = ({ navRef = null }: { navRef?: MutableRefObject<HTMLElement | null> | null } = {}) => (
<>
{/* Sucursal, como en el sistema legacy */}
<p className="text-center text-sm font-medium text-white py-3 shrink-0" style={{ backgroundColor: 'var(--sidebar-group-bg)' }}>
Rosarito
</p>
<nav className="flex-1 overflow-y-auto py-2">
<nav ref={navRef} className="flex-1 overflow-y-auto py-2">
{quickItems.length > 0 && quick.mode === 'menu' && (
<div className="mb-2">
<div className="flex items-center justify-between px-4 py-2" style={{ backgroundColor: 'var(--sidebar-group-bg)' }}>
@@ -405,6 +439,7 @@ export const ClasicoLayout: FC<LayoutProps> = ({ children, title: _title, subtit
<Link
to={child.href}
onClick={() => setSidebarOpen(false)}
data-active={active ? '1' : undefined}
className="flex-1 flex items-center pl-11 pr-4 py-2 text-[13px] transition-colors"
style={{
backgroundColor: active ? 'rgba(0,0,0,0.25)' : 'transparent',
@@ -533,7 +568,7 @@ export const ClasicoLayout: FC<LayoutProps> = ({ children, title: _title, subtit
className="hidden lg:flex flex-col w-[230px] fixed top-[50px] bottom-0 left-0 z-20"
style={{ backgroundColor: 'var(--sidebar-bg)' }}
>
<SidebarContent />
{renderSidebar({ navRef: navDesktopRef })}
</aside>
{/* Drawer móvil */}
@@ -546,7 +581,7 @@ export const ClasicoLayout: FC<LayoutProps> = ({ children, title: _title, subtit
}`}
style={{ backgroundColor: 'var(--sidebar-bg)' }}
>
<SidebarContent />
{renderSidebar({ navRef: navMobileRef })}
</aside>
{/* Contenido */}