From 20982aa0773f99afd714da355fb76ae6d6dca87d Mon Sep 17 00:00:00 2001 From: Esteban Date: Thu, 12 Feb 2026 15:26:01 -0600 Subject: [PATCH] feat: responsive sidebar, client selector and layout with alert/device counts --- src/app/(dashboard)/layout.tsx | 127 ++++++++-- src/components/layout/Header.tsx | 44 +++- src/components/layout/Sidebar.tsx | 280 ++++++++++++----------- src/components/layout/SidebarItem.tsx | 69 ++++++ src/components/layout/SidebarSection.tsx | 24 ++ 5 files changed, 386 insertions(+), 158 deletions(-) create mode 100644 src/components/layout/SidebarItem.tsx create mode 100644 src/components/layout/SidebarSection.tsx diff --git a/src/app/(dashboard)/layout.tsx b/src/app/(dashboard)/layout.tsx index b596cb9..ae7ee41 100644 --- a/src/app/(dashboard)/layout.tsx +++ b/src/app/(dashboard)/layout.tsx @@ -1,37 +1,132 @@ 'use client' -import { useState, useEffect } from 'react' +import { useEffect, useState } from 'react' +import { useRouter } from 'next/navigation' import Sidebar from '@/components/layout/Sidebar' import Header from '@/components/layout/Header' +import { SelectedClientProvider, useSelectedClient } from '@/components/providers/SelectedClientProvider' +import { trpc } from '@/lib/trpc-client' export default function DashboardLayout({ children, }: { children: React.ReactNode }) { - const [alertasActivas, setAlertasActivas] = useState(0) - const [user, setUser] = useState({ - nombre: 'Admin', - email: 'admin@example.com', - rol: 'SUPER_ADMIN', + const router = useRouter() + + const meQuery = trpc.auth.me.useQuery(undefined, { + retry: false, + staleTime: 60 * 1000, + }) + const logoutMutation = trpc.auth.logout.useMutation({ + onSuccess: () => { + window.location.href = '/login' + }, }) useEffect(() => { - // TODO: Cargar alertas activas desde API - // TODO: Cargar usuario desde sesion - }, []) + if (meQuery.isError) { + router.push('/login') + } + }, [meQuery.isError, router]) - const handleLogout = async () => { - // TODO: Implementar logout - window.location.href = '/login' + const handleLogout = () => { + logoutMutation.mutate() } + if (meQuery.isLoading || meQuery.isError) { + return ( +
+
Cargando...
+
+ ) + } + + const user = meQuery.data + if (!user) return null + + return ( + + {children} + + ) +} + +function DashboardContent({ + user, + onLogout, + children, +}: { + user: { nombre: string; email: string; rol: string } + onLogout: () => void + children: React.ReactNode +}) { + return ( + + + {children} + + + ) +} + +function DashboardContentInner({ + user, + onLogout, + children, +}: { + user: { nombre: string; email: string; rol: string } + onLogout: () => void + children: React.ReactNode +}) { + const { selectedClientId } = useSelectedClient() + const clienteId = selectedClientId ?? undefined + + const activeAlertsCountQuery = trpc.alertas.conteoActivas.useQuery( + { clienteId }, + { refetchOnWindowFocus: true, staleTime: 30 * 1000 } + ) + const activeAlertsCount = activeAlertsCountQuery.data?.total ?? 0 + + const devicesCountQuery = trpc.equipos.list.useQuery( + { clienteId, page: 1, limit: 1 }, + { refetchOnWindowFocus: true, staleTime: 30 * 1000 } + ) + const devicesCount = devicesCountQuery.data?.pagination?.total ?? 0 + + const clientsQuery = trpc.clientes.list.useQuery( + { limit: 100 }, + { staleTime: 60 * 1000 } + ) + const clients = (clientsQuery.data?.clientes ?? []).map((c) => ({ + id: c.id, + nombre: c.nombre, + codigo: c.codigo, + })) + + const [sidebarOpen, setSidebarOpen] = useState(false) + return (
- -
-
-
+ setSidebarOpen(false)} + /> +
+
setSidebarOpen(true)} + /> +
{children}
diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx index f8df12f..5cf19a6 100644 --- a/src/components/layout/Header.tsx +++ b/src/components/layout/Header.tsx @@ -1,9 +1,12 @@ 'use client' import { useState } from 'react' -import { Bell, Search, User, LogOut, Settings, ChevronDown } from 'lucide-react' +import { Bell, Search, User, LogOut, Settings, ChevronDown, Menu } from 'lucide-react' import { cn } from '@/lib/utils' import ClientSelector from './ClientSelector' +import { useSelectedClient } from '@/components/providers/SelectedClientProvider' + +export type HeaderClient = { id: string; nombre: string; codigo: string } interface HeaderProps { user?: { @@ -13,17 +16,36 @@ interface HeaderProps { rol: string } onLogout?: () => void + clients?: HeaderClient[] + showAllClientsOption?: boolean + onOpenSidebar?: () => void } -export default function Header({ user, onLogout }: HeaderProps) { +export default function Header({ + user, + onLogout, + clients = [], + showAllClientsOption = false, + onOpenSidebar, +}: HeaderProps) { const [showUserMenu, setShowUserMenu] = useState(false) const [showNotifications, setShowNotifications] = useState(false) + const { selectedClientId, setSelectedClientId } = useSelectedClient() return ( -
- {/* Search */} -
-
+
+
+ {onOpenSidebar != null && ( + + )} +
- {/* Client Selector */} - +
{/* Right section */} @@ -79,7 +105,7 @@ export default function Header({ user, onLogout }: HeaderProps) { />
diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx index 0c88001..17a9634 100644 --- a/src/components/layout/Sidebar.tsx +++ b/src/components/layout/Sidebar.tsx @@ -1,88 +1,112 @@ 'use client' -import { useState } from 'react' -import Link from 'next/link' import { usePathname } from 'next/navigation' +import Link from 'next/link' import { LayoutDashboard, Monitor, - Smartphone, + Video, + Terminal, + FolderOpen, + Gauge, + Package, Network, + Smartphone, AlertTriangle, FileText, Settings, - Users, - Building2, - ChevronLeft, - ChevronRight, Activity, + X, } from 'lucide-react' import { cn } from '@/lib/utils' +import SidebarItem, { type BadgeType } from './SidebarItem' +import SidebarSection from './SidebarSection' -interface NavItem { +export interface SidebarMenuItem { label: string href: string icon: React.ReactNode - badge?: number + badge?: { type: BadgeType; value: string | number } } -const navItems: NavItem[] = [ - { - label: 'Dashboard', - href: '/', - icon: , - }, - { - label: 'Equipos', - href: '/equipos', - icon: , - }, - { - label: 'Celulares', - href: '/celulares', - icon: , - }, - { - label: 'Red', - href: '/red', - icon: , - }, - { - label: 'Alertas', - href: '/alertas', - icon: , - }, - { - label: 'Reportes', - href: '/reportes', - icon: , - }, -] +export interface SidebarMenuSection { + label: string + items: SidebarMenuItem[] +} -const adminItems: NavItem[] = [ +const menuConfig: SidebarMenuSection[] = [ { - label: 'Clientes', - href: '/clientes', - icon: , + label: 'PRINCIPAL', + items: [ + { label: 'Dashboard', href: '/', icon: }, + { + label: 'Dispositivos', + href: '/devices', + icon: , + badge: { type: 'red', value: 10 }, + }, + { + label: 'Sesiones', + href: '/sesiones', + icon: