agendapro/admin/src/app/page.tsx

239 lines
11 KiB
TypeScript

'use client';
import { useState, useEffect, useRef } from 'react';
import { LayoutDashboard, Users, CreditCard, Package, Settings, Shield, TrendingUp, Bell, LogOut, User, CheckCircle, AlertCircle } from 'lucide-react';
import AdminDashboard from '@/components/AdminDashboard';
import Tenants from '@/components/Tenants';
import Plans from '@/components/Plans';
import Payments from '@/components/Payments';
import AdminSettings from '@/components/AdminSettings';
const navItems = [
{ section: 'Visão Geral' },
{ id: 'dashboard', label: 'Dashboard', icon: LayoutDashboard },
{ section: 'Gestão' },
{ id: 'tenants', label: 'Assinantes', icon: Users },
{ id: 'plans', label: 'Planos', icon: Package },
{ id: 'payments', label: 'Pagamentos', icon: CreditCard },
{ section: 'Sistema' },
{ id: 'settings', label: 'Configurações', icon: Settings },
];
const pages: Record<string, { title: string; subtitle: string }> = {
dashboard: { title: 'Dashboard', subtitle: 'Visão geral das assinaturas' },
tenants: { title: 'Assinantes', subtitle: 'Gerenciar estabelecimentos' },
plans: { title: 'Planos', subtitle: 'Gerenciar planos de assinatura' },
payments: { title: 'Pagamentos', subtitle: 'Histórico de cobranças' },
settings: { title: 'Configurações', subtitle: 'Configurações do sistema' },
};
const mockNotifications = [
{ id: 1, title: 'Nova Assinatura', message: 'Barbearia do Zé contratou o plano Starter', type: 'success', time: '5 min atrás', read: false },
{ id: 2, title: 'Falha no Pagamento', message: 'Salão Glamour não conseguiu renovar a assinatura', type: 'warning', time: '1 hora atrás', read: true },
{ id: 3, title: 'Ticket de Suporte', message: 'Novo chamado aberto por Studio Bella Nails', type: 'info', time: '2 horas atrás', read: true },
];
export default function AdminPage() {
const [currentPage, setCurrentPage] = useState('dashboard');
const [showNotifications, setShowNotifications] = useState(false);
const [showProfile, setShowProfile] = useState(false);
const [notifications, setNotifications] = useState(mockNotifications);
const notifRef = useRef<HTMLDivElement>(null);
const profileRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const handleClickOutside = (e: MouseEvent) => {
if (notifRef.current && !notifRef.current.contains(e.target as Node)) setShowNotifications(false);
if (profileRef.current && !profileRef.current.contains(e.target as Node)) setShowProfile(false);
};
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
const playNotificationSound = () => {
try {
const audioCtx = new (window.AudioContext || (window as any).webkitAudioContext)();
const oscillator = audioCtx.createOscillator();
const gainNode = audioCtx.createGain();
oscillator.type = 'sine';
oscillator.frequency.setValueAtTime(880, audioCtx.currentTime); // A5
oscillator.frequency.exponentialRampToValueAtTime(1760, audioCtx.currentTime + 0.1); // A6
gainNode.gain.setValueAtTime(0.1, audioCtx.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.3);
oscillator.connect(gainNode);
gainNode.connect(audioCtx.destination);
oscillator.start();
oscillator.stop(audioCtx.currentTime + 0.3);
} catch (e) {
console.log('Audio not supported or blocked');
}
};
const handleNotificationClick = () => {
if (!showNotifications) {
const unreadCount = notifications.filter(n => !n.read).length;
if (unreadCount > 0) {
playNotificationSound();
}
}
setShowNotifications(!showNotifications);
setShowProfile(false);
};
const handleProfileClick = () => {
setShowProfile(!showProfile);
setShowNotifications(false);
};
const markAllAsRead = () => {
setNotifications(notifications.map(n => ({ ...n, read: true })));
};
const logout = () => {
window.location.href = '/';
};
const unreadCount = notifications.filter(n => !n.read).length;
const renderPage = () => {
switch (currentPage) {
case 'dashboard': return <AdminDashboard onNavigate={setCurrentPage} />;
case 'tenants': return <Tenants />;
case 'plans': return <Plans />;
case 'payments': return <Payments />;
case 'settings': return <AdminSettings />;
default: return <AdminDashboard onNavigate={setCurrentPage} />;
}
};
return (
<div className="app-layout">
<aside className="sidebar">
<div className="sidebar-header">
<div className="sidebar-logo">
<div className="logo-icon"><Shield size={20} /></div>
<div>
<div className="logo-text">AgendaPRO</div>
<div className="logo-sub">ADMIN PANEL</div>
</div>
</div>
</div>
<nav className="sidebar-nav">
{navItems.map((item, i) => {
if ('section' in item && item.section) return <div key={`s-${i}`} className="nav-section-title">{item.section}</div>;
if ('id' in item && item.id) {
const Icon = item.icon!;
return (
<button key={item.id} className={`nav-item ${currentPage === item.id ? 'active' : ''}`}
onClick={() => setCurrentPage(item.id!)}>
<Icon size={20} /> {item.label}
</button>
);
}
return null;
})}
</nav>
<div style={{ padding: '16px', borderTop: '1px solid var(--border-color)' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', padding: '10px', borderRadius: 'var(--radius-sm)', background: 'var(--bg-card)' }}>
<div className="avatar" style={{ width: 32, height: 32, fontSize: 12 }}>SA</div>
<div style={{ flex: 1 }}>
<div style={{ fontSize: '13px', fontWeight: 600 }}>Super Admin</div>
<div style={{ fontSize: '11px', color: 'var(--text-muted)' }}>admin@agendapro.com</div>
</div>
</div>
</div>
</aside>
<main className="main-content">
<header className="topbar">
<div>
<h1>{pages[currentPage]?.title}</h1>
<p>{pages[currentPage]?.subtitle}</p>
</div>
<div className="flex-gap">
<div style={{ position: 'relative' }} ref={notifRef}>
<button className="btn-ghost btn-icon" style={{ position: 'relative' }} onClick={handleNotificationClick}>
<Bell size={20} />
{unreadCount > 0 && (
<span style={{ position: 'absolute', top: 4, right: 4, width: 10, height: 10, borderRadius: '50%', background: 'var(--danger)', border: '2px solid var(--bg-card)' }} />
)}
</button>
{showNotifications && (
<div className="card" style={{
position: 'absolute', top: '100%', right: 0, marginTop: '8px', width: '320px',
padding: 0, zIndex: 100, boxShadow: 'var(--shadow-lg)', border: '1px solid var(--border-color)',
animation: 'slideUp 0.2s ease-out forwards'
}}>
<div className="flex-between" style={{ padding: '12px 16px', borderBottom: '1px solid var(--border-color)' }}>
<h3 style={{ fontSize: '14px', fontWeight: 600 }}>Notificações</h3>
{unreadCount > 0 && (
<button className="btn-ghost" style={{ fontSize: '12px', padding: '4px 8px' }} onClick={markAllAsRead}>
Marcar lidas
</button>
)}
</div>
<div style={{ maxHeight: '300px', overflowY: 'auto' }}>
{notifications.map(n => (
<div key={n.id} style={{
padding: '12px 16px', borderBottom: '1px solid var(--border-color)',
background: n.read ? 'transparent' : 'rgba(59, 130, 246, 0.05)',
display: 'flex', gap: '12px', alignItems: 'flex-start'
}}>
<div style={{
width: '32px', height: '32px', borderRadius: '50%', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0,
background: n.type === 'success' ? 'rgba(34, 197, 94, 0.1)' : n.type === 'warning' ? 'rgba(245, 158, 11, 0.1)' : 'rgba(59, 130, 246, 0.1)',
color: n.type === 'success' ? 'var(--success)' : n.type === 'warning' ? 'var(--warning)' : 'var(--primary)'
}}>
{n.type === 'success' ? <CheckCircle size={16} /> : n.type === 'warning' ? <AlertCircle size={16} /> : <Bell size={16} />}
</div>
<div>
<div style={{ fontSize: '13px', fontWeight: n.read ? 500 : 600, color: 'var(--text-primary)' }}>{n.title}</div>
<div style={{ fontSize: '12px', color: 'var(--text-secondary)', marginTop: '2px', lineHeight: 1.4 }}>{n.message}</div>
<div style={{ fontSize: '11px', color: 'var(--text-muted)', marginTop: '4px' }}>{n.time}</div>
</div>
</div>
))}
</div>
</div>
)}
</div>
<div style={{ position: 'relative' }} ref={profileRef}>
<div className="avatar" style={{ width: 36, height: 36, fontSize: 13, cursor: 'pointer', transition: 'transform 0.2s' }} onClick={handleProfileClick}>SA</div>
{showProfile && (
<div className="card" style={{
position: 'absolute', top: '100%', right: 0, marginTop: '8px', width: '240px',
padding: '8px', zIndex: 100, boxShadow: 'var(--shadow-lg)', border: '1px solid var(--border-color)',
animation: 'slideUp 0.2s ease-out forwards'
}}>
<div style={{ padding: '12px', borderBottom: '1px solid var(--border-color)', marginBottom: '8px' }}>
<div style={{ fontWeight: 600, fontSize: '14px' }}>Super Admin</div>
<div style={{ color: 'var(--text-muted)', fontSize: '12px', marginTop: '2px' }}>admin@agendapro.com</div>
</div>
<button className="btn-ghost" style={{ width: '100%', justifyContent: 'flex-start', padding: '10px 12px', fontSize: '13px' }}>
<User size={16} style={{ marginRight: '8px' }} /> Meu Perfil
</button>
<button className="btn-ghost" onClick={logout} style={{ width: '100%', justifyContent: 'flex-start', padding: '10px 12px', fontSize: '13px', color: 'var(--danger)' }}>
<LogOut size={16} style={{ marginRight: '8px' }} /> Sair do Painel
</button>
</div>
)}
</div>
</div>
</header>
<div className="page-content fade-in" key={currentPage}>
{renderPage()}
</div>
</main>
</div>
);
}