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:
2026-10-11 10:49:44 +00:00
parent 9286ab8ee2
commit 8ff13b9c2a
33 changed files with 3873 additions and 22 deletions

View File

@@ -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