535 lines
22 KiB
TypeScript
535 lines
22 KiB
TypeScript
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,
|
|
Wallet,
|
|
CreditCard,
|
|
ShoppingCart,
|
|
Receipt,
|
|
Store,
|
|
BarChart3,
|
|
Settings,
|
|
Menu,
|
|
X,
|
|
ChevronRight,
|
|
LogOut,
|
|
MessageSquare,
|
|
Target,
|
|
Cake,
|
|
Boxes,
|
|
ClipboardList,
|
|
ClipboardPlus,
|
|
Banknote,
|
|
Percent,
|
|
Star,
|
|
Pencil,
|
|
ChevronUp,
|
|
ChevronDown,
|
|
MessageCircle,
|
|
CircleDollarSign,
|
|
PanelsTopLeft,
|
|
} from 'lucide-react';
|
|
import { Button, Modal, Input } from './ui';
|
|
import QuickLinksDock, { QuickLinksModePicker } from './QuickLinksDock';
|
|
import GlobalSearch from './GlobalSearch';
|
|
import ThemePicker from './ThemePicker';
|
|
import NotificationBell from './NotificationBell';
|
|
import { useAuth, ROLE_LABELS } from '../lib/auth';
|
|
import { useChatUnread } from '../lib/chat';
|
|
import { useQuickLinks } from '../lib/quicklinks';
|
|
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: 'Visitas', icon: ClipboardList, href: '/visitas' },
|
|
{ label: 'Consultas', icon: ClipboardPlus, href: '/consultas' },
|
|
{ label: 'Mis ventas', icon: CircleDollarSign, href: '/mis-ventas' },
|
|
{ label: 'Médicos', icon: Stethoscope, href: '/medicos' },
|
|
{ label: 'Servicios', icon: Briefcase, href: '/servicios' },
|
|
],
|
|
},
|
|
{
|
|
label: 'Operaciones',
|
|
items: [
|
|
{ label: 'Punto de Venta', icon: Store, href: '/pos' },
|
|
{ label: 'Ventas', icon: ShoppingCart, href: '/ventas' },
|
|
{ label: 'Pagos', icon: CreditCard, href: '/pagos' },
|
|
{ label: 'Adeudos', icon: Banknote, href: '/reportes?tab=adeudos' },
|
|
{ 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: 'Comisiones', icon: Percent, href: '/reportes?tab=comisiones' },
|
|
{ label: 'Configuración', icon: Settings, href: '/configuracion' },
|
|
],
|
|
},
|
|
{
|
|
label: 'Comunicación',
|
|
items: [
|
|
{ label: 'Chat', icon: MessageCircle, href: '/chat' },
|
|
{ label: 'Cumpleañeros', icon: Cake, href: '/cumpleanos' },
|
|
],
|
|
},
|
|
{
|
|
label: 'WACRM',
|
|
items: [
|
|
{ label: 'Mensajes', icon: MessageSquare, href: '/wacrm/messages' },
|
|
{ label: 'Leads', icon: Target, href: '/wacrm/leads' },
|
|
],
|
|
},
|
|
];
|
|
|
|
export const SkeenLayout: FC<LayoutProps> = ({ children, title, subtitle }) => {
|
|
const location = useLocation();
|
|
const navigate = useNavigate();
|
|
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(
|
|
() =>
|
|
menuGroups
|
|
.map((g) => ({ ...g, items: g.items.filter((it) => canSee(it.href)) }))
|
|
.filter((g) => g.items.length > 0),
|
|
[canSee, user]
|
|
);
|
|
|
|
// Accesos rápidos personalizables (localStorage por usuario)
|
|
const quick = useQuickLinks();
|
|
const dockItems = useMemo(
|
|
() => menuGroups.flatMap((g) => g.items).filter((it) => canSee(it.href)),
|
|
[canSee]
|
|
);
|
|
const quickItems = useMemo(
|
|
() =>
|
|
quick.links
|
|
.filter((href) => canSee(href))
|
|
.map((href) => {
|
|
for (const g of menuGroups) {
|
|
const found = g.items.find((it) => it.href === href);
|
|
if (found) return found;
|
|
}
|
|
return null;
|
|
})
|
|
.filter((it): it is (typeof menuGroups)[number]['items'][number] => it !== null),
|
|
[quick.links, canSee]
|
|
);
|
|
|
|
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 = ({ collapsed = false, showToggle = true }: { collapsed?: boolean; showToggle?: boolean }) => (
|
|
<>
|
|
<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' && 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>
|
|
<span className="flex items-center gap-1">
|
|
<QuickLinksModePicker direction="down" align="right" />
|
|
<button
|
|
type="button"
|
|
onClick={() => quick.setEditing(!quick.editing)}
|
|
className="text-white/40 hover:text-white transition-colors"
|
|
title={quick.editing ? 'Terminar edición' : 'Editar accesos rápidos'}
|
|
>
|
|
{quick.editing ? <X size={13} /> : <Pencil size={12} />}
|
|
</button>
|
|
</span>
|
|
</div>
|
|
<ul className="space-y-1">
|
|
{quickItems.map((item, idx) => {
|
|
const Icon = item.icon;
|
|
const active = isActive(item.href);
|
|
return (
|
|
<li key={item.href} className="flex items-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 ${
|
|
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>
|
|
</Link>
|
|
{quick.editing && (
|
|
<span className="flex items-center gap-0.5 shrink-0">
|
|
<button type="button" onClick={() => quick.move(item.href, -1)} disabled={idx === 0}
|
|
className="text-white/40 hover:text-white disabled:opacity-30 transition-colors" title="Subir">
|
|
<ChevronUp size={14} />
|
|
</button>
|
|
<button type="button" onClick={() => quick.move(item.href, 1)} disabled={idx === quickItems.length - 1}
|
|
className="text-white/40 hover:text-white disabled:opacity-30 transition-colors" title="Bajar">
|
|
<ChevronDown size={14} />
|
|
</button>
|
|
<button type="button" onClick={() => quick.remove(item.href)}
|
|
className="text-white/40 hover:text-rose-400 transition-colors" title="Quitar">
|
|
<X size={14} />
|
|
</button>
|
|
</span>
|
|
)}
|
|
</li>
|
|
);
|
|
})}
|
|
</ul>
|
|
</div>
|
|
)}
|
|
{visibleGroups.map((group) => (
|
|
<div key={group.label} className="mb-6">
|
|
{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 ${collapsed ? 'justify-center' : 'gap-1'}`}>
|
|
<Link
|
|
to={item.href}
|
|
onClick={() => setSidebarOpen(false)}
|
|
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} />
|
|
{!collapsed && <span>{item.label}</span>}
|
|
{item.href === '/chat' && chatUnread > 0 && (
|
|
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 && !collapsed && <ChevronRight size={14} className={item.href === '/chat' && chatUnread > 0 ? '' : 'ml-auto'} />}
|
|
</Link>
|
|
{!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>
|
|
);
|
|
})}
|
|
</ul>
|
|
</div>
|
|
))}
|
|
</nav>
|
|
|
|
<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>
|
|
{!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"
|
|
title="Cerrar sesión"
|
|
aria-label="Cerrar sesión"
|
|
>
|
|
<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 fixed h-full z-20 transition-all duration-200 ${collapsed ? 'w-16' : 'w-64'}`}
|
|
style={{ backgroundColor: 'var(--sidebar-bg)' }}
|
|
>
|
|
<SidebarContent collapsed={collapsed} />
|
|
</aside>
|
|
|
|
{sidebarOpen && (
|
|
<div className="fixed inset-0 bg-black/40 z-30 lg:hidden" onClick={() => setSidebarOpen(false)} aria-hidden="true" />
|
|
)}
|
|
|
|
<aside
|
|
className={`fixed inset-y-0 left-0 w-64 z-40 transform transition-transform duration-200 ease-in-out lg:hidden flex flex-col ${
|
|
sidebarOpen ? 'translate-x-0' : '-translate-x-full'
|
|
}`}
|
|
style={{ backgroundColor: 'var(--sidebar-bg)' }}
|
|
>
|
|
<div className="h-20 flex items-center justify-between px-5 border-b border-white/10 shrink-0">
|
|
<img src={LOGO_SKEEN} alt="SKEEN" className="h-7 w-auto object-contain" />
|
|
<Button variant="ghost" size="sm" onClick={() => setSidebarOpen(false)} aria-label="Cerrar menú">
|
|
<X size={22} />
|
|
</Button>
|
|
</div>
|
|
<SidebarContent />
|
|
</aside>
|
|
|
|
<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">
|
|
<div className="flex items-center space-x-3 min-w-0">
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
className="lg:hidden"
|
|
style={{ color: 'var(--text-heading)' }}
|
|
onClick={() => setSidebarOpen(true)}
|
|
aria-label="Abrir menú"
|
|
>
|
|
<Menu size={22} />
|
|
</Button>
|
|
<div className="min-w-0">
|
|
<h2 className="text-lg font-bold truncate font-heading tracking-tight" style={{ color: 'var(--text-heading)' }}>{title}</h2>
|
|
{subtitle && <p className="text-xs hidden sm:block truncate" style={{ color: 'var(--text-muted)' }}>{subtitle}</p>}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center space-x-2 sm:space-x-3 shrink-0">
|
|
{goal && goal.goal > 0 && (
|
|
<button
|
|
onClick={hasRole('admin') ? openGoalModal : undefined}
|
|
className={`hidden md:flex flex-col w-44 xl:w-56 text-left rounded-xl border px-3 py-1.5 shadow-card ${hasRole('admin') ? 'cursor-pointer' : 'cursor-default'}`}
|
|
style={{ borderColor: 'var(--border)', backgroundColor: 'var(--surface)' }}
|
|
title={hasRole('admin') ? 'Click para editar la meta' : 'Meta de ventas del mes'}
|
|
>
|
|
<div className="flex items-center justify-between text-[11px] mb-1" style={{ color: 'var(--text-muted)' }}>
|
|
<span className="flex items-center gap-1"><Target size={12} /> Meta del mes</span>
|
|
<span className="font-bold" style={{ color: 'var(--text-heading)' }}>{Math.round(goal.pct)}%</span>
|
|
</div>
|
|
<div className="w-full h-1.5 rounded-full overflow-hidden" style={{ backgroundColor: 'var(--bg)' }}>
|
|
<div className="h-full rounded-full transition-all" style={{ width: `${pctClamped}%`, backgroundColor: goal.pct >= 100 ? 'var(--success)' : 'var(--accent)' }} />
|
|
</div>
|
|
<div className="flex items-center justify-between text-[11px] mt-1">
|
|
<span className="font-semibold" style={{ color: 'var(--text-heading)' }}>{fmtMoney(goal.current)}</span>
|
|
<span style={{ color: 'var(--text-muted)' }}>de {fmtMoney(goal.goal)}</span>
|
|
</div>
|
|
</button>
|
|
)}
|
|
{goal && goal.goal <= 0 && hasRole('admin') && (
|
|
<Button variant="outline" size="sm" className="hidden md:inline-flex" onClick={openGoalModal}>
|
|
<Target size={14} className="mr-1.5" /> Definir meta
|
|
</Button>
|
|
)}
|
|
<GlobalSearch className="hidden md:block" />
|
|
<NotificationBell buttonClassName="p-2 rounded-full relative transition text-theme-muted hover:bg-theme-accent-bg hover:text-theme-heading" />
|
|
<ThemePicker />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<main className={`flex-1 w-full px-4 sm:px-6 lg:px-8 py-6 sm:py-8 ${quick.mode === 'right' ? 'lg:pr-20' : ''} ${quick.mode === 'bottom' ? 'pb-20' : ''}`}>
|
|
{children}
|
|
</main>
|
|
</div>
|
|
|
|
<QuickLinksDock items={dockItems} />
|
|
|
|
<Modal
|
|
isOpen={goalModalOpen}
|
|
onClose={() => setGoalModalOpen(false)}
|
|
title="Meta de ventas del mes"
|
|
maxWidth="sm"
|
|
footer={
|
|
<>
|
|
<Button variant="outline" onClick={() => setGoalModalOpen(false)}>Cancelar</Button>
|
|
<Button onClick={saveGoal} loading={goalSaving}>Guardar meta</Button>
|
|
</>
|
|
}
|
|
>
|
|
<div className="space-y-3">
|
|
<p className="text-sm" style={{ color: 'var(--text-muted)' }}>Define el objetivo de ventas (MXN) para el mes en curso. La barra del header muestra el avance en tiempo real.</p>
|
|
<Input label="Meta mensual (MXN)" type="number" min={0} step={1000} value={goalInput} onChange={(e) => setGoalInput(e.target.value)} placeholder="ej. 500000" />
|
|
</div>
|
|
</Modal>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default SkeenLayout;
|