import type { FC, ReactNode } from 'react'; import { useEffect, useMemo, useState } from 'react'; import { Link, useLocation, useNavigate } from 'react-router-dom'; import { LayoutDashboard, Calendar, Users, Stethoscope, Briefcase, Package, Wallet, CreditCard, ShoppingCart, Receipt, BarChart3, Settings, Bell, Search, Menu, X, ChevronRight, LogOut, MessageSquare, Target, Cake, Boxes, ShieldCheck, Palette, } from 'lucide-react'; import { Button, Modal, Input } from './ui'; import { useAuth, ROLE_LABELS } from '../lib/auth'; import { useTheme } from '../lib/theme'; import { odooApi } from '../services/odoo'; interface LayoutProps { children: ReactNode; title: string; subtitle?: string; } const LOGO_SKEEN = '/skeen-brand/logos/Logo%20Completo%20Blanco.png'; const menuGroups = [ { label: 'Principal', items: [ { label: 'Dashboard', icon: LayoutDashboard, href: '/' }, { label: 'Agenda', icon: Calendar, href: '/agenda' }, ], }, { label: 'Clínica', items: [ { label: 'Pacientes', icon: Users, href: '/pacientes' }, { label: 'Médicos', icon: Stethoscope, href: '/medicos' }, { label: 'Servicios', icon: Briefcase, href: '/servicios' }, { label: 'Productos', icon: Package, href: '/productos' }, ], }, { label: 'Operaciones', items: [ { label: 'Ventas', icon: ShoppingCart, href: '/ventas' }, { label: 'Pagos', icon: CreditCard, href: '/pagos' }, { label: 'Monedero', icon: Wallet, href: '/monedero' }, { label: 'Inventario', icon: Boxes, href: '/inventario' }, { label: 'Cortes de Caja', icon: Receipt, href: '/cortes' }, ], }, { label: 'Analítica', items: [ { label: 'Reportes', icon: BarChart3, href: '/reportes' }, { label: 'Configuración', icon: Settings, href: '/configuracion' }, ], }, { label: 'Comunicación', items: [ { label: 'Cumpleañeros', icon: Cake, href: '/cumpleanos' }, ], }, { label: 'Administración', items: [ { label: 'Usuarios', icon: ShieldCheck, href: '/usuarios' }, ], }, { label: 'WACRM', items: [ { label: 'Mensajes', icon: MessageSquare, href: '/wacrm/messages' }, { label: 'Leads', icon: Target, href: '/wacrm/leads' }, ], }, ]; export const SkeenLayout: FC = ({ children, title, subtitle }) => { const location = useLocation(); const navigate = useNavigate(); const { user, canSee, hasRole, logout } = useAuth(); const { toggleTheme } = useTheme(); const [sidebarOpen, setSidebarOpen] = useState(false); const visibleGroups = useMemo( () => menuGroups .map((g) => ({ ...g, items: g.items.filter((it) => canSee(it.href)) })) .filter((g) => g.items.length > 0), [canSee, user] ); const initials = (user?.name || 'US') .split(' ') .filter(Boolean) .slice(0, 2) .map((w) => w[0]?.toUpperCase()) .join(''); const handleLogout = async () => { await logout(); navigate('/login', { replace: true }); }; const [goal, setGoal] = useState<{ goal: number; current: number; pct: number } | null>(null); const [goalModalOpen, setGoalModalOpen] = useState(false); const [goalInput, setGoalInput] = useState(''); const [goalSaving, setGoalSaving] = useState(false); useEffect(() => { let alive = true; const fetchGoal = () => { odooApi.getSalesGoal() .then((res) => { if (alive && res.status === 'success') { setGoal({ goal: res.goal, current: res.current, pct: res.pct }); } }) .catch(() => {}); }; fetchGoal(); const id = setInterval(fetchGoal, 60000); return () => { alive = false; clearInterval(id); }; }, []); const openGoalModal = () => { setGoalInput(goal?.goal ? String(goal.goal) : ''); setGoalModalOpen(true); }; const saveGoal = async () => { const v = parseFloat(goalInput); if (Number.isNaN(v) || v < 0) return; try { setGoalSaving(true); await odooApi.setSalesGoal(v); setGoal((g) => (g ? { ...g, goal: v, pct: g.current && v ? (g.current / v) * 100 : 0 } : g)); setGoalModalOpen(false); } catch { // ignore } finally { setGoalSaving(false); } }; const fmtMoney = (n: number) => n >= 1000000 ? `$${(n / 1000000).toFixed(1)}M` : n >= 1000 ? `$${(n / 1000).toFixed(0)}k` : `$${n.toFixed(0)}`; const pctClamped = Math.min(goal?.pct ?? 0, 100); useEffect(() => { if (!sidebarOpen) return; const originalOverflow = document.body.style.overflow; document.body.style.overflow = 'hidden'; return () => { document.body.style.overflow = originalOverflow; }; }, [sidebarOpen]); const isActive = (href: string) => { if (href === '/') return location.pathname === '/'; return location.pathname === href || location.pathname.startsWith(`${href}/`); }; const SidebarContent = () => ( <>
SKEEN
{initials || 'US'}

{user?.name || 'Usuario'}

{user ? ROLE_LABELS[user.role] : ''}

); return (
{sidebarOpen && (
setSidebarOpen(false)} aria-hidden="true" /> )}

{title}

{subtitle &&

{subtitle}

}
{goal && goal.goal > 0 && ( )} {goal && goal.goal <= 0 && hasRole('admin') && ( )}
{children}
setGoalModalOpen(false)} title="Meta de ventas del mes" maxWidth="sm" footer={ <> } >

Define el objetivo de ventas (MXN) para el mes en curso. La barra del header muestra el avance en tiempo real.

setGoalInput(e.target.value)} placeholder="ej. 500000" />
); }; export default SkeenLayout;