// ============================================================
// JuFlow — Sidebar + Header Layout (responsive + safe-area)
// ============================================================
const { useState: useSt, useEffect: useEf } = React;
const U2 = window.JFUtils;
const NAV_ITEMS = [
{ id: 'dashboard', label: 'Dashboard', icon: 'LayoutDashboard' },
{ id: 'agenda', label: 'Agenda', icon: 'Calendar' },
{ id: 'clientes', label: 'Clientes', icon: 'Users' },
{ id: 'atendimentos', label: 'Atendimentos', icon: 'ClipboardList' },
{ id: 'servicos', label: 'Serviços', icon: 'Scissors' },
{ id: 'profissionais', label: 'Profissionais', icon: 'UserCheck' },
{ id: 'financeiro', label: 'Financeiro', icon: 'DollarSign' },
{ id: 'marketing', label: 'Marketing', icon: 'Megaphone' },
{ id: 'relatorios', label: 'Relatórios', icon: 'BarChart3' },
{ id: 'configuracoes', label: 'Configurações', icon: 'Settings' },
];
// ─── useIsMobile hook ─────────────────────────────────────
const useIsMobile = (breakpoint = 900) => {
const [isMobile, setIsMobile] = useSt(typeof window !== 'undefined' && window.matchMedia(`(max-width: ${breakpoint}px)`).matches);
useEf(() => {
const mq = window.matchMedia(`(max-width: ${breakpoint}px)`);
const handler = e => setIsMobile(e.matches);
if (mq.addEventListener) mq.addEventListener('change', handler);
else mq.addListener(handler);
return () => {
if (mq.removeEventListener) mq.removeEventListener('change', handler);
else mq.removeListener(handler);
};
}, [breakpoint]);
return isMobile;
};
const SidebarLogo = ({ collapsed }) => (
<>
{!collapsed && (
)}
>
);
// ─── SIDEBAR ──────────────────────────────────────────────
const Sidebar = ({ page, onNavigate, collapsed, onToggle, isMobile, mobileOpen, onMobileClose }) => {
const W = isMobile ? 280 : (collapsed ? 68 : 240);
const showText = isMobile ? true : !collapsed;
const showCollapseToggle = !isMobile;
const showMobileClose = isMobile;
// Mobile: drawer behavior (off-canvas)
const mobileTransform = isMobile && !mobileOpen ? 'translateX(-100%)' : 'translateX(0)';
return (
<>
{/* Mobile backdrop */}
{isMobile && mobileOpen && (
)}
{/* Logo */}
{showMobileClose && (
)}
{/* Toggle button (desktop only) */}
{showCollapseToggle && (
)}
{/* Nav */}
{/* User avatar at bottom */}
A
{showText && (
Admin Demo
demo@juflow.com
)}
>
);
};
// ─── NOTIFICATION PANEL ───────────────────────────────────
const NotificationPanel = ({ onClose, appointments }) => {
const today = window.JF.CLIENTS.filter(c => U2.isBirthdayThisMonth(c.birthdate)).slice(0, 3);
const upcoming = appointments
.filter(a => ['confirmed','pending'].includes(a.status) && new Date(a.datetime) >= window.JF.TODAY)
.sort((a,b) => new Date(a.datetime) - new Date(b.datetime))
.slice(0, 4);
return (
Notificações
{today.length > 0 && (
🎂 Aniversariantes
{today.map(c => (
{c.name} — {U2.formatDate(c.birthdate, 'dd/MM')}
))}
)}
{upcoming.map(a => {
const client = U2.getClient(a.clientId);
const service = U2.getService(a.serviceId);
return (
{client?.name}
{service?.name} · {U2.formatDateTime(a.datetime)}
);
})}
);
};
// ─── HEADER ───────────────────────────────────────────────
const Header = ({ page, appointments, onNavigate, onLogout, onMenuOpen, isMobile }) => {
const [notifOpen, setNotifOpen] = useSt(false);
const [searchVal, setSearchVal] = useSt('');
const [searchResults, setSearchResults] = useSt([]);
const [searchFocus, setSearchFocus] = useSt(false);
const pageLabels = {
dashboard:'Dashboard', agenda:'Agenda', clientes:'Clientes',
atendimentos:'Atendimentos', servicos:'Serviços', profissionais:'Profissionais',
financeiro:'Financeiro', marketing:'Marketing', relatorios:'Relatórios', configuracoes:'Configurações',
};
const todayCount = appointments.filter(a =>
U2.isSameDay(a.datetime, window.JF.TODAY) && ['confirmed','pending'].includes(a.status)
).length;
useEf(() => {
if (searchVal.length < 2) { setSearchResults([]); return; }
const q = searchVal.toLowerCase();
const clients = window.JF.CLIENTS.filter(c => c.name.toLowerCase().includes(q)).slice(0,3)
.map(c => ({ type:'client', label:c.name, sub:'Cliente', id:c.id, icon:'User' }));
const services = window.JF.SERVICES.filter(s => s.name.toLowerCase().includes(q)).slice(0,2)
.map(s => ({ type:'service', label:s.name, sub:s.category, id:s.id, icon:'Scissors' }));
setSearchResults([...clients, ...services]);
}, [searchVal]);
return (
{/* Hamburger (mobile) */}
{isMobile && (
)}
{/* Page title */}
{pageLabels[page] || page}
{!isMobile &&
{U2.formatDateLong(window.JF.TODAY)}
}
{/* Search - hidden on mobile */}
{!isMobile && (
{searchFocus && searchResults.length > 0 && (
{searchResults.map((r, i) => (
{ setSearchVal(''); setSearchResults([]); if(r.type==='client') onNavigate('clientes'); }}
style={{
display:'flex', alignItems:'center', gap:10,
padding:'10px 14px', cursor:'pointer', fontSize:13,
}}
onMouseEnter={e => e.currentTarget.style.background='#F9FAFB'}
onMouseLeave={e => e.currentTarget.style.background='#fff'}
>
))}
)}
)}
{/* Today badge */}
{todayCount > 0 && !isMobile && (
{todayCount} hoje
)}
{/* Notifications */}
{notifOpen && (
setNotifOpen(false)} appointments={appointments} />
)}
{/* User menu */}
A
) : (
)
}
items={[
{ label:'Perfil', icon:'User', onClick:() => {} },
{ label:'Configurações', icon:'Settings', onClick:() => onNavigate('configuracoes') },
'divider',
{ label:'Sair', icon:'LogOut', onClick:onLogout, danger:true },
]}
/>
);
};
Object.assign(window, { Sidebar, Header, useIsMobile });