Files
HoruxStrategyKimi/apps/web/src/components/layout/Sidebar.tsx
HORUX360 a9b1994c48 feat: Implement Phase 1 & 2 - Full monorepo architecture
## Backend API (apps/api)
- Express.js server with TypeScript
- JWT authentication with access/refresh tokens
- Multi-tenant middleware (schema per tenant)
- Complete CRUD routes: auth, cfdis, transactions, contacts, categories, metrics, alerts
- SAT integration: CFDI 4.0 XML parser, FIEL authentication
- Metrics engine: 50+ financial metrics (Core, Startup, Enterprise)
- Rate limiting, CORS, Helmet security

## Frontend Web (apps/web)
- Next.js 14 with App Router
- Authentication pages: login, register, forgot-password
- Dashboard layout with Sidebar and Header
- Dashboard pages: overview, cash-flow, revenue, expenses, metrics
- Zustand stores for auth and UI state
- Theme support with flash prevention

## Database Package (packages/database)
- PostgreSQL migrations with multi-tenant architecture
- Public schema: plans, tenants, users, sessions, subscriptions
- Tenant schema: sat_credentials, cfdis, transactions, contacts, accounts, alerts
- Tenant management functions
- Seed data for plans and super admin

## Shared Package (packages/shared)
- TypeScript types: auth, tenant, financial, metrics, reports
- Zod validation schemas for all entities
- Utility functions for formatting

## UI Package (packages/ui)
- Chart components: LineChart, BarChart, AreaChart, PieChart
- Data components: DataTable, MetricCard, KPICard, AlertBadge
- PeriodSelector and Skeleton components

## Infrastructure
- Docker Compose: PostgreSQL 15, Redis 7, MinIO, Mailhog
- Makefile with 25+ development commands
- Development scripts: dev-setup.sh, dev-down.sh
- Complete .env.example template

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-31 11:05:24 +00:00

307 lines
8.1 KiB
TypeScript

'use client';
import React from 'react';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { cn } from '@/lib/utils';
import { useUIStore } from '@/stores/ui.store';
import {
LayoutDashboard,
LineChart,
Wallet,
History,
Settings,
HelpCircle,
ChevronLeft,
ChevronRight,
TrendingUp,
Bot,
Shield,
Bell,
} from 'lucide-react';
/**
* Item de navegación
*/
interface NavItem {
label: string;
href: string;
icon: React.ReactNode;
badge?: string | number;
children?: NavItem[];
}
/**
* Grupos de navegación
*/
interface NavGroup {
label?: string;
items: NavItem[];
}
/**
* Navegación principal
*/
const navigation: NavGroup[] = [
{
items: [
{
label: 'Dashboard',
href: '/dashboard',
icon: <LayoutDashboard className="h-5 w-5" />,
},
],
},
{
label: 'Trading',
items: [
{
label: 'Estrategias',
href: '/strategies',
icon: <Bot className="h-5 w-5" />,
badge: 3,
},
{
label: 'Portfolio',
href: '/portfolio',
icon: <Wallet className="h-5 w-5" />,
},
{
label: 'Mercados',
href: '/markets',
icon: <TrendingUp className="h-5 w-5" />,
},
{
label: 'Historial',
href: '/history',
icon: <History className="h-5 w-5" />,
},
],
},
{
label: 'Analisis',
items: [
{
label: 'Performance',
href: '/analytics',
icon: <LineChart className="h-5 w-5" />,
},
{
label: 'Riesgo',
href: '/risk',
icon: <Shield className="h-5 w-5" />,
},
],
},
{
label: 'Sistema',
items: [
{
label: 'Notificaciones',
href: '/notifications',
icon: <Bell className="h-5 w-5" />,
badge: 5,
},
{
label: 'Configuracion',
href: '/settings',
icon: <Settings className="h-5 w-5" />,
},
{
label: 'Ayuda',
href: '/help',
icon: <HelpCircle className="h-5 w-5" />,
},
],
},
];
/**
* Componente NavLink
*/
interface NavLinkProps {
item: NavItem;
collapsed: boolean;
}
const NavLink: React.FC<NavLinkProps> = ({ item, collapsed }) => {
const pathname = usePathname();
const isActive = pathname === item.href || pathname.startsWith(`${item.href}/`);
return (
<Link
href={item.href}
className={cn(
'flex items-center gap-3 px-3 py-2.5 rounded-lg',
'text-sm font-medium transition-all duration-200',
'group relative',
isActive
? 'bg-primary-50 text-primary-700 dark:bg-primary-900/30 dark:text-primary-300'
: 'text-slate-600 hover:bg-slate-100 hover:text-slate-900 dark:text-slate-400 dark:hover:bg-slate-700/50 dark:hover:text-white',
collapsed && 'justify-center px-2'
)}
>
{/* Active Indicator */}
{isActive && (
<span className="absolute left-0 top-1/2 -translate-y-1/2 w-1 h-6 bg-primary-600 dark:bg-primary-400 rounded-r-full" />
)}
{/* Icon */}
<span
className={cn(
'flex-shrink-0 transition-colors',
isActive
? 'text-primary-600 dark:text-primary-400'
: 'text-slate-400 group-hover:text-slate-600 dark:text-slate-500 dark:group-hover:text-slate-300'
)}
>
{item.icon}
</span>
{/* Label */}
{!collapsed && (
<span className="flex-1 truncate">{item.label}</span>
)}
{/* Badge */}
{item.badge && !collapsed && (
<span className="flex-shrink-0 px-2 py-0.5 text-xs font-semibold rounded-full bg-primary-100 text-primary-700 dark:bg-primary-900/50 dark:text-primary-300">
{item.badge}
</span>
)}
{/* Tooltip when collapsed */}
{collapsed && (
<span className="absolute left-full ml-2 px-2 py-1 text-sm font-medium text-white bg-slate-900 dark:bg-slate-700 rounded opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all whitespace-nowrap z-50">
{item.label}
{item.badge && (
<span className="ml-2 px-1.5 py-0.5 text-xs rounded-full bg-primary-500 text-white">
{item.badge}
</span>
)}
</span>
)}
</Link>
);
};
/**
* Componente Sidebar
*
* Barra lateral de navegación con soporte para colapsado y grupos de navegación.
*/
export const Sidebar: React.FC = () => {
const { sidebarOpen, sidebarCollapsed, toggleSidebarCollapsed, isMobile, setSidebarOpen } = useUIStore();
// En mobile, cerrar al hacer click en overlay
const handleOverlayClick = () => {
if (isMobile) {
setSidebarOpen(false);
}
};
return (
<>
{/* Overlay mobile */}
{isMobile && sidebarOpen && (
<div
className="fixed inset-0 bg-black/50 z-40 lg:hidden"
onClick={handleOverlayClick}
aria-hidden="true"
/>
)}
{/* Sidebar */}
<aside
className={cn(
'fixed top-0 left-0 z-50 h-full',
'bg-white dark:bg-slate-900',
'border-r border-slate-200 dark:border-slate-700',
'flex flex-col',
'transition-all duration-300 ease-in-out',
// Width
sidebarCollapsed ? 'w-20' : 'w-64',
// Mobile
isMobile && !sidebarOpen && '-translate-x-full',
isMobile && sidebarOpen && 'translate-x-0',
// Desktop
!isMobile && 'translate-x-0'
)}
>
{/* Header */}
<div className={cn(
'flex items-center h-16 px-4',
'border-b border-slate-200 dark:border-slate-700',
sidebarCollapsed && 'justify-center'
)}>
{/* Logo */}
<Link href="/dashboard" className="flex items-center gap-3">
<div className="flex-shrink-0 w-10 h-10 rounded-xl bg-horux-gradient flex items-center justify-center">
<span className="text-white font-bold text-xl">H</span>
</div>
{!sidebarCollapsed && (
<span className="text-xl font-bold text-slate-900 dark:text-white">
Horux
</span>
)}
</Link>
</div>
{/* Navigation */}
<nav className="flex-1 overflow-y-auto py-4 px-3 space-y-6">
{navigation.map((group, idx) => (
<div key={idx}>
{/* Group Label */}
{group.label && !sidebarCollapsed && (
<h3 className="px-3 mb-2 text-xs font-semibold text-slate-400 dark:text-slate-500 uppercase tracking-wider">
{group.label}
</h3>
)}
{/* Separator when collapsed */}
{group.label && sidebarCollapsed && (
<div className="my-2 border-t border-slate-200 dark:border-slate-700" />
)}
{/* Items */}
<div className="space-y-1">
{group.items.map((item) => (
<NavLink key={item.href} item={item} collapsed={sidebarCollapsed} />
))}
</div>
</div>
))}
</nav>
{/* Footer - Collapse Button */}
{!isMobile && (
<div className="p-3 border-t border-slate-200 dark:border-slate-700">
<button
onClick={toggleSidebarCollapsed}
className={cn(
'flex items-center gap-3 w-full px-3 py-2.5 rounded-lg',
'text-sm font-medium text-slate-600 dark:text-slate-400',
'hover:bg-slate-100 dark:hover:bg-slate-700/50',
'transition-all duration-200',
sidebarCollapsed && 'justify-center'
)}
aria-label={sidebarCollapsed ? 'Expandir sidebar' : 'Colapsar sidebar'}
>
{sidebarCollapsed ? (
<ChevronRight className="h-5 w-5" />
) : (
<>
<ChevronLeft className="h-5 w-5" />
<span>Colapsar</span>
</>
)}
</button>
</div>
)}
</aside>
</>
);
};
export default Sidebar;