diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx
index 7b86120..df22936 100644
--- a/frontend/src/components/Layout.tsx
+++ b/frontend/src/components/Layout.tsx
@@ -1,239 +1,100 @@
-import { useState, useEffect } from "react";
-import { Link, Outlet, useLocation, useNavigate } from "react-router-dom";
-import {
- Sparkles,
- Telescope,
- Calculator,
- BarChart3,
- Landmark,
- PiggyBank,
- Droplets,
- Layers,
- LogOut,
- TrendingUp,
- Wallet,
- Menu,
- RefreshCw,
- Sun,
- Moon,
- Eye,
- EyeOff,
- type LucideIcon,
-} from "lucide-react";
-import { useQuery } from "@tanstack/react-query";
+import { Link, Outlet, useLocation } from "react-router-dom";
+import { Eye, EyeOff, Moon, Sun } from "lucide-react";
-import { getSyncStatus } from "@/lib/api";
+import { titleFor } from "@/lib/navigation";
import { useTheme } from "@/contexts/theme-context";
import { usePrivacy } from "@/contexts/privacy-context";
-import { useAuth } from "@/AuthContext";
+import { AppSidebar } from "@/components/app-sidebar";
import { Button } from "@/components/ui/button";
import {
- Sheet,
- SheetContent,
- SheetHeader,
- SheetTitle,
- SheetClose,
-} from "@/components/ui/sheet";
+ Breadcrumb,
+ BreadcrumbItem,
+ BreadcrumbLink,
+ BreadcrumbList,
+ BreadcrumbPage,
+ BreadcrumbSeparator,
+} from "@/components/ui/breadcrumb";
import { Separator } from "@/components/ui/separator";
-import { cn } from "@/lib/utils";
-
-interface NavSection {
- label: string;
- items: { to: string; icon: LucideIcon; label: string }[];
-}
-
-const navSections: NavSection[] = [
- {
- label: "General",
- items: [
- { to: "/asistente", icon: Sparkles, label: "Asistente" },
- { to: "/sync", icon: RefreshCw, label: "Sincronización" },
- ],
- },
- {
- label: "Finanzas",
- items: [
- { to: "/budget", icon: Calculator, label: "Presupuesto" },
- { to: "/salarios", icon: Landmark, label: "Salarios" },
- { to: "/financiamientos", icon: Layers, label: "Financiamientos" },
- { to: "/pensions", icon: PiggyBank, label: "Pensiones" },
- { to: "/proyecciones", icon: TrendingUp, label: "Proyecciones" },
- { to: "/planificador", icon: Telescope, label: "Planificador" },
- { to: "/analytics", icon: BarChart3, label: "Analytics" },
- ],
- },
- {
- label: "Servicios",
- items: [
- { to: "/servicios-municipales", icon: Droplets, label: "Municipalidad" },
- ],
- },
-];
-
-function SidebarNav({ onNavigate }: { onNavigate?: () => void }) {
- const { pathname } = useLocation();
- const isActive = (to: string) =>
- pathname === to || pathname.startsWith(`${to}/`);
-
- // Surface stalled ingestion sources as a dot on the Sincronización item.
- const syncQ = useQuery({
- queryKey: ['sync-status'],
- queryFn: () => getSyncStatus().then((r) => r.data),
- staleTime: 5 * 60_000,
- });
- const syncWarnings = syncQ.data?.warnings ?? 0;
-
- return (
-
- );
-}
+import {
+ SidebarInset,
+ SidebarProvider,
+ SidebarTrigger,
+} from "@/components/ui/sidebar";
export default function Layout() {
const { theme, toggleTheme } = useTheme();
const { privacyMode, togglePrivacy } = usePrivacy();
- const { logout } = useAuth();
- const navigate = useNavigate();
- const [mobileOpen, setMobileOpen] = useState(false);
-
- const handleLogout = async () => {
- await logout();
- navigate("/login", { replace: true });
- };
+ const { pathname } = useLocation();
+ const title = titleFor(pathname);
return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- WealthySmart
-
-
-
-
-
-
- setMobileOpen(false)} />
-
-
-
- }>
-
-
-
-
-
-
-
-
-
-
+
+
);
}
diff --git a/frontend/src/components/app-sidebar.tsx b/frontend/src/components/app-sidebar.tsx
new file mode 100644
index 0000000..310268b
--- /dev/null
+++ b/frontend/src/components/app-sidebar.tsx
@@ -0,0 +1,100 @@
+import { Wallet } from "lucide-react";
+import { Link, useLocation } from "react-router-dom";
+import { useQuery } from "@tanstack/react-query";
+
+import { getSyncStatus } from "@/lib/api";
+import { isNavActive, navSections } from "@/lib/navigation";
+import { NavUser } from "@/components/nav-user";
+import {
+ Sidebar,
+ SidebarContent,
+ SidebarFooter,
+ SidebarGroup,
+ SidebarGroupContent,
+ SidebarGroupLabel,
+ SidebarHeader,
+ SidebarMenu,
+ SidebarMenuBadge,
+ SidebarMenuButton,
+ SidebarMenuItem,
+ SidebarRail,
+ useSidebar,
+} from "@/components/ui/sidebar";
+
+export function AppSidebar(props: React.ComponentProps
) {
+ const { pathname } = useLocation();
+ const { setOpenMobile } = useSidebar();
+
+ // Surface stalled ingestion sources as a dot on the Sincronización item.
+ const syncQ = useQuery({
+ queryKey: ["sync-status"],
+ queryFn: () => getSyncStatus().then((r) => r.data),
+ staleTime: 5 * 60_000,
+ });
+ const syncWarnings = syncQ.data?.warnings ?? 0;
+
+ return (
+
+
+
+
+ }
+ >
+
+
+
+
+ WealthySmart
+
+
+
+
+
+
+
+ {navSections.map((section) => (
+
+ {section.label}
+
+
+ {section.items.map(({ to, icon: Icon, label }) => (
+
+ setOpenMobile(false)} />
+ }
+ >
+
+ {label}
+
+ {to === "/sync" && syncWarnings > 0 && (
+
+
+
+ )}
+
+ ))}
+
+
+
+ ))}
+
+
+
+
+
+
+
+ );
+}
diff --git a/frontend/src/components/nav-user.tsx b/frontend/src/components/nav-user.tsx
new file mode 100644
index 0000000..32fbdc7
--- /dev/null
+++ b/frontend/src/components/nav-user.tsx
@@ -0,0 +1,108 @@
+import { ChevronsUpDown, Eye, EyeOff, LogOut, Moon, Sun } from "lucide-react";
+import { useNavigate } from "react-router-dom";
+import { useQuery } from "@tanstack/react-query";
+
+import { getMe } from "@/lib/api";
+import { useTheme } from "@/contexts/theme-context";
+import { usePrivacy } from "@/contexts/privacy-context";
+import { useAuth } from "@/AuthContext";
+import { Avatar, AvatarFallback } from "@/components/ui/avatar";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuGroup,
+ DropdownMenuItem,
+ DropdownMenuLabel,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu";
+import {
+ SidebarMenu,
+ SidebarMenuButton,
+ SidebarMenuItem,
+ useSidebar,
+} from "@/components/ui/sidebar";
+
+export function NavUser() {
+ const { isMobile } = useSidebar();
+ const { theme, toggleTheme } = useTheme();
+ const { privacyMode, togglePrivacy } = usePrivacy();
+ const { logout } = useAuth();
+ const navigate = useNavigate();
+
+ const meQ = useQuery({
+ queryKey: ["me"],
+ queryFn: () => getMe().then((r) => r.data),
+ staleTime: Infinity,
+ });
+ const username = meQ.data?.username ?? "Usuario";
+ const initials = username.slice(0, 2).toUpperCase();
+
+ const handleLogout = async () => {
+ await logout();
+ navigate("/login", { replace: true });
+ };
+
+ return (
+
+
+
+
+ }
+ >
+
+ {initials}
+
+
+ {username}
+
+ WealthySmart
+
+
+
+
+
+
+
+
+
+ {initials}
+
+
+ {username}
+
+ WealthySmart
+
+
+
+
+
+
+
+
+ {privacyMode ? : }
+ {privacyMode ? "Desactivar modo privado" : "Modo privado"}
+
+
+ {theme === "dark" ? : }
+ {theme === "dark" ? "Tema claro" : "Tema oscuro"}
+
+
+
+ void handleLogout()}>
+
+ Cerrar sesión
+
+
+
+
+
+ );
+}