From fd28bf67d8c3f2bf7e7996d2dc3f554253bd240a Mon Sep 17 00:00:00 2001 From: Ivan Date: Sun, 1 Feb 2026 06:30:25 +0000 Subject: [PATCH] feat(layout): add admin panel layout with sidebar and header - Add Sidebar component with navigation items (dashboard, bookings, tournaments, pos, clients, memberships, reports, settings) - Add Header component with SiteSwitcher and user info/logout - Add SiteSwitcher component for SUPER_ADMIN multi-site selection - Add admin layout wrapper with AuthProvider - Add placeholder Dashboard page Co-Authored-By: Claude Opus 4.5 --- apps/web/app/(admin)/dashboard/page.tsx | 10 ++ apps/web/app/(admin)/layout.tsx | 21 ++++ apps/web/components/layout/header.tsx | 40 +++++++ apps/web/components/layout/sidebar.tsx | 74 ++++++++++++ apps/web/components/layout/site-switcher.tsx | 119 +++++++++++++++++++ 5 files changed, 264 insertions(+) create mode 100644 apps/web/app/(admin)/dashboard/page.tsx create mode 100644 apps/web/app/(admin)/layout.tsx create mode 100644 apps/web/components/layout/header.tsx create mode 100644 apps/web/components/layout/sidebar.tsx create mode 100644 apps/web/components/layout/site-switcher.tsx diff --git a/apps/web/app/(admin)/dashboard/page.tsx b/apps/web/app/(admin)/dashboard/page.tsx new file mode 100644 index 0000000..b84e4c8 --- /dev/null +++ b/apps/web/app/(admin)/dashboard/page.tsx @@ -0,0 +1,10 @@ +export default function DashboardPage() { + return ( +
+

Dashboard

+

+ Bienvenido al panel de administración de Padel Pro. +

+
+ ); +} diff --git a/apps/web/app/(admin)/layout.tsx b/apps/web/app/(admin)/layout.tsx new file mode 100644 index 0000000..7da2c19 --- /dev/null +++ b/apps/web/app/(admin)/layout.tsx @@ -0,0 +1,21 @@ +import { AuthProvider } from '@/components/providers/auth-provider'; +import { Sidebar } from '@/components/layout/sidebar'; +import { Header } from '@/components/layout/header'; + +export default function AdminLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + +
+ +
+
+
{children}
+
+
+
+ ); +} diff --git a/apps/web/components/layout/header.tsx b/apps/web/components/layout/header.tsx new file mode 100644 index 0000000..05bec0e --- /dev/null +++ b/apps/web/components/layout/header.tsx @@ -0,0 +1,40 @@ +'use client'; + +import { useSession, signOut } from 'next-auth/react'; +import { LogOut } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { SiteSwitcher } from './site-switcher'; + +export function Header() { + const { data: session } = useSession(); + + const handleLogout = () => { + signOut({ callbackUrl: '/login' }); + }; + + const userRole = session?.user?.role || ''; + const displayRole = userRole + .replace(/_/g, ' ') + .toLowerCase() + .replace(/\b\w/g, (l) => l.toUpperCase()); + + return ( +
+
+ +
+ +
+
+

{session?.user?.name || 'Usuario'}

+

{displayRole}

+
+ +
+
+ ); +} + +export default Header; diff --git a/apps/web/components/layout/sidebar.tsx b/apps/web/components/layout/sidebar.tsx new file mode 100644 index 0000000..69d2c9b --- /dev/null +++ b/apps/web/components/layout/sidebar.tsx @@ -0,0 +1,74 @@ +'use client'; + +import Link from 'next/link'; +import { usePathname } from 'next/navigation'; +import { + LayoutDashboard, + Calendar, + Trophy, + ShoppingCart, + Users, + CreditCard, + BarChart3, + Settings, +} from 'lucide-react'; +import { cn } from '@/lib/utils'; + +interface NavItem { + label: string; + href: string; + icon: React.ComponentType<{ className?: string }>; +} + +const navItems: NavItem[] = [ + { label: 'Dashboard', href: '/admin/dashboard', icon: LayoutDashboard }, + { label: 'Reservas', href: '/admin/bookings', icon: Calendar }, + { label: 'Torneos', href: '/admin/tournaments', icon: Trophy }, + { label: 'Ventas', href: '/admin/pos', icon: ShoppingCart }, + { label: 'Clientes', href: '/admin/clients', icon: Users }, + { label: 'Membresías', href: '/admin/memberships', icon: CreditCard }, + { label: 'Reportes', href: '/admin/reports', icon: BarChart3 }, + { label: 'Configuración', href: '/admin/settings', icon: Settings }, +]; + +export function Sidebar() { + const pathname = usePathname(); + + return ( + + ); +} + +export default Sidebar; diff --git a/apps/web/components/layout/site-switcher.tsx b/apps/web/components/layout/site-switcher.tsx new file mode 100644 index 0000000..f18e3b7 --- /dev/null +++ b/apps/web/components/layout/site-switcher.tsx @@ -0,0 +1,119 @@ +'use client'; + +import { useState, useEffect, useRef } from 'react'; +import { useSession } from 'next-auth/react'; +import { MapPin, ChevronDown, Check } from 'lucide-react'; +import { cn } from '@/lib/utils'; + +interface Site { + id: string; + name: string; +} + +export function SiteSwitcher() { + const { data: session } = useSession(); + const [sites, setSites] = useState([]); + const [selectedSiteId, setSelectedSiteId] = useState(null); + const [isOpen, setIsOpen] = useState(false); + const [isLoading, setIsLoading] = useState(true); + const dropdownRef = useRef(null); + + const isSuperAdmin = session?.user?.role === 'SUPER_ADMIN'; + + useEffect(() => { + async function fetchSites() { + try { + const response = await fetch('/api/sites'); + if (response.ok) { + const data = await response.json(); + setSites(data.data || []); + } + } catch (error) { + console.error('Failed to fetch sites:', error); + } finally { + setIsLoading(false); + } + } + + fetchSites(); + }, []); + + useEffect(() => { + function handleClickOutside(event: MouseEvent) { + if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { + setIsOpen(false); + } + } + + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + }, []); + + const selectedSite = sites.find((site) => site.id === selectedSiteId); + const displayName = selectedSiteId ? selectedSite?.name : 'Todas las sedes'; + + // For non-SUPER_ADMIN users, just show their assigned site + if (!isSuperAdmin) { + const userSiteName = sites.length > 0 ? sites[0]?.name : 'Cargando...'; + return ( +
+ + {isLoading ? 'Cargando...' : userSiteName} +
+ ); + } + + // For SUPER_ADMIN, show dropdown to select site + return ( +
+ + + {isOpen && ( +
+ +
+ {sites.map((site) => ( + + ))} +
+ )} +
+ ); +} + +export default SiteSwitcher;