## 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>
307 lines
8.1 KiB
TypeScript
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;
|