fase 2.6: membresias y suscripciones (modulo skeen_membresias: planes recurrentes con sesiones incluidas o ilimitadas, alta/cobro/renovacion/congelamiento/cancelacion, consumo de sesiones desde POS y expediente, cron de vencimientos y avisos, reporte de ingresos recurrentes); apagado por defecto
This commit is contained in:
@@ -30,6 +30,7 @@ import {
|
||||
DollarSign,
|
||||
CircleDollarSign,
|
||||
TicketCheck,
|
||||
Repeat,
|
||||
} from 'lucide-react';
|
||||
import { Button, Modal, Input } from './ui';
|
||||
import QuickLinksDock, { QuickLinksModePicker } from './QuickLinksDock';
|
||||
@@ -37,6 +38,7 @@ import GlobalSearch from './GlobalSearch';
|
||||
import ThemePicker from './ThemePicker';
|
||||
import NotificationBell from './NotificationBell';
|
||||
import { useAuth, ROLE_LABELS } from '../lib/auth';
|
||||
import { useClinicSettings } from '../lib/clinic-settings';
|
||||
import { useChatUnread } from '../lib/chat';
|
||||
import { useQuickLinks } from '../lib/quicklinks';
|
||||
import { odooApi } from '../services/odoo';
|
||||
@@ -140,21 +142,32 @@ const menuTree: MenuEntry[] = [
|
||||
{ label: 'Mis ventas', icon: CircleDollarSign, href: '/mis-ventas' },
|
||||
];
|
||||
|
||||
// Hojas planas (para accesos rápidos y dock)
|
||||
const menuLeaves = menuTree.flatMap((e) =>
|
||||
e.children ? e.children.map((c) => ({ label: c.label, icon: e.icon, href: c.href })) : [{ label: e.label, icon: e.icon, href: e.href as string }]
|
||||
);
|
||||
|
||||
export const ClasicoLayout: FC<LayoutProps> = ({ children, title: _title, subtitle: _subtitle }) => {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const { user, canSee, hasRole, logout, brand } = useAuth();
|
||||
const { membresias: membresiasCfg } = useClinicSettings();
|
||||
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||||
const chatUnread = useChatUnread();
|
||||
|
||||
// Membresías solo cuando la clínica enciende `membresias.activo`.
|
||||
const menuTreeT = useMemo<MenuEntry[]>(
|
||||
() => (membresiasCfg.activo
|
||||
? [...menuTree, { label: 'Membresías', icon: Repeat, href: '/membresias' }]
|
||||
: menuTree),
|
||||
[membresiasCfg.activo]
|
||||
);
|
||||
const menuLeavesT = useMemo(
|
||||
() => menuTreeT.flatMap((e) =>
|
||||
e.children
|
||||
? e.children.map((c) => ({ label: c.label, icon: e.icon, href: c.href }))
|
||||
: [{ label: e.label, icon: e.icon, href: e.href as string }]),
|
||||
[menuTreeT]
|
||||
);
|
||||
|
||||
const visibleLeaves = useMemo(
|
||||
() => menuLeaves.filter((it) => canSee(it.href)),
|
||||
[canSee]
|
||||
() => menuLeavesT.filter((it) => canSee(it.href)),
|
||||
[canSee, menuLeavesT]
|
||||
);
|
||||
|
||||
// Accesos rápidos personalizables (localStorage por usuario)
|
||||
@@ -164,9 +177,9 @@ export const ClasicoLayout: FC<LayoutProps> = ({ children, title: _title, subtit
|
||||
() =>
|
||||
quick.links
|
||||
.filter((href) => canSee(href))
|
||||
.map((href) => menuLeaves.find((it) => it.href === href) ?? null)
|
||||
.filter((it): it is (typeof menuLeaves)[number] => it !== null),
|
||||
[quick.links, canSee]
|
||||
.map((href) => menuLeavesT.find((it) => it.href === href) ?? null)
|
||||
.filter((it): it is (typeof menuLeavesT)[number] => it !== null),
|
||||
[quick.links, canSee, menuLeavesT]
|
||||
);
|
||||
|
||||
const initials = (user?.name || 'US')
|
||||
@@ -251,7 +264,7 @@ export const ClasicoLayout: FC<LayoutProps> = ({ children, title: _title, subtit
|
||||
// Grupos de submenú abiertos (auto-abre el de la ruta activa)
|
||||
const [openGroups, setOpenGroups] = useState<string[]>(() => {
|
||||
const current = `${location.pathname}${location.search}`;
|
||||
return menuTree
|
||||
return menuTreeT
|
||||
.filter((e) => e.children?.some((c) => current.startsWith(c.href.split('?')[0]) && (!c.href.includes('?') || current.includes(c.href.split('?')[1]))))
|
||||
.map((e) => e.label);
|
||||
});
|
||||
@@ -269,7 +282,7 @@ export const ClasicoLayout: FC<LayoutProps> = ({ children, title: _title, subtit
|
||||
// 1) El grupo que contiene la ruta activa siempre queda abierto
|
||||
useEffect(() => {
|
||||
const current = `${location.pathname}${location.search}`;
|
||||
const abiertos = menuTree
|
||||
const abiertos = menuTreeT
|
||||
.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])));
|
||||
@@ -361,7 +374,7 @@ export const ClasicoLayout: FC<LayoutProps> = ({ children, title: _title, subtit
|
||||
</div>
|
||||
)}
|
||||
<ul>
|
||||
{menuTree.map((entry) => {
|
||||
{menuTreeT.map((entry) => {
|
||||
const Icon = entry.icon;
|
||||
// Hojas visibles de este entry
|
||||
const leaves = entry.children
|
||||
|
||||
Reference in New Issue
Block a user