From e1751ef694d7b042e78ab17b93f88368fb5bc196 Mon Sep 17 00:00:00 2001 From: "Roberto.silva" Date: Thu, 22 Jan 2026 15:46:04 -0600 Subject: [PATCH] Se corrigen incidencias 8 y 13 --- package.json | 1 + src/components/ui/context-menu.tsx | 250 +++++++++++++++++++ src/routeTree.gen.ts | 21 +- src/routes/planes/$planId/_detalle/datos.tsx | 2 +- src/routes/planes/$planId/_detalle/route.tsx | 166 ++++++++---- 5 files changed, 396 insertions(+), 44 deletions(-) create mode 100644 src/components/ui/context-menu.tsx diff --git a/package.json b/package.json index 5449226..11d96e4 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "@radix-ui/react-avatar": "^1.1.11", "@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-collapsible": "^1.1.12", + "@radix-ui/react-context-menu": "^2.2.16", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-label": "^2.1.8", diff --git a/src/components/ui/context-menu.tsx b/src/components/ui/context-menu.tsx new file mode 100644 index 0000000..624c588 --- /dev/null +++ b/src/components/ui/context-menu.tsx @@ -0,0 +1,250 @@ +import * as React from "react" +import * as ContextMenuPrimitive from "@radix-ui/react-context-menu" +import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react" + +import { cn } from "@/lib/utils" + +function ContextMenu({ + ...props +}: React.ComponentProps) { + return +} + +function ContextMenuTrigger({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function ContextMenuGroup({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function ContextMenuPortal({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function ContextMenuSub({ + ...props +}: React.ComponentProps) { + return +} + +function ContextMenuRadioGroup({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function ContextMenuSubTrigger({ + className, + inset, + children, + ...props +}: React.ComponentProps & { + inset?: boolean +}) { + return ( + + {children} + + + ) +} + +function ContextMenuSubContent({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function ContextMenuContent({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +function ContextMenuItem({ + className, + inset, + variant = "default", + ...props +}: React.ComponentProps & { + inset?: boolean + variant?: "default" | "destructive" +}) { + return ( + + ) +} + +function ContextMenuCheckboxItem({ + className, + children, + checked, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ) +} + +function ContextMenuRadioItem({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ) +} + +function ContextMenuLabel({ + className, + inset, + ...props +}: React.ComponentProps & { + inset?: boolean +}) { + return ( + + ) +} + +function ContextMenuSeparator({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function ContextMenuShortcut({ + className, + ...props +}: React.ComponentProps<"span">) { + return ( + + ) +} + +export { + ContextMenu, + ContextMenuTrigger, + ContextMenuContent, + ContextMenuItem, + ContextMenuCheckboxItem, + ContextMenuRadioItem, + ContextMenuLabel, + ContextMenuSeparator, + ContextMenuShortcut, + ContextMenuGroup, + ContextMenuPortal, + ContextMenuSub, + ContextMenuSubContent, + ContextMenuSubTrigger, + ContextMenuRadioGroup, +} diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index 3f8ce7e..37b426a 100644 --- a/src/routeTree.gen.ts +++ b/src/routeTree.gen.ts @@ -15,6 +15,7 @@ import { Route as IndexRouteImport } from './routes/index' import { Route as PlanesPlanesListRouteRouteImport } from './routes/planes/PlanesListRoute' import { Route as DemoTanstackQueryRouteImport } from './routes/demo/tanstack-query' import { Route as PlanesListaRouteRouteImport } from './routes/planes/_lista/route' +import { Route as PlanesPlanIdIndexRouteImport } from './routes/planes/$planId/index' import { Route as PlanesListaNuevoRouteImport } from './routes/planes/_lista/nuevo' import { Route as PlanesPlanIdAsignaturasRouteRouteImport } from './routes/planes/$planId/asignaturas/route' import { Route as PlanesPlanIdDetalleRouteRouteImport } from './routes/planes/$planId/_detalle/route' @@ -60,6 +61,11 @@ const PlanesListaRouteRoute = PlanesListaRouteRouteImport.update({ path: '/planes', getParentRoute: () => rootRouteImport, } as any) +const PlanesPlanIdIndexRoute = PlanesPlanIdIndexRouteImport.update({ + id: '/planes/$planId/', + path: '/planes/$planId/', + getParentRoute: () => rootRouteImport, +} as any) const PlanesListaNuevoRoute = PlanesListaNuevoRouteImport.update({ id: '/nuevo', path: '/nuevo', @@ -152,6 +158,7 @@ export interface FileRoutesByFullPath { '/planes/$planId': typeof PlanesPlanIdDetalleRouteRouteWithChildren '/planes/$planId/asignaturas': typeof PlanesPlanIdAsignaturasListaRouteRouteWithChildren '/planes/nuevo': typeof PlanesListaNuevoRoute + '/planes/$planId/': typeof PlanesPlanIdIndexRoute '/planes/$planId/asignaturas/$asignaturaId': typeof PlanesPlanIdAsignaturasAsignaturaIdRouteRoute '/planes/$planId/datos': typeof PlanesPlanIdDetalleDatosRoute '/planes/$planId/documento': typeof PlanesPlanIdDetalleDocumentoRoute @@ -170,7 +177,7 @@ export interface FileRoutesByTo { '/planes': typeof PlanesListaRouteRouteWithChildren '/demo/tanstack-query': typeof DemoTanstackQueryRoute '/planes/PlanesListRoute': typeof PlanesPlanesListRouteRoute - '/planes/$planId': typeof PlanesPlanIdDetalleRouteRouteWithChildren + '/planes/$planId': typeof PlanesPlanIdIndexRoute '/planes/nuevo': typeof PlanesListaNuevoRoute '/planes/$planId/asignaturas/$asignaturaId': typeof PlanesPlanIdAsignaturasAsignaturaIdRouteRoute '/planes/$planId/asignaturas': typeof PlanesPlanIdAsignaturasIndexRoute @@ -194,6 +201,7 @@ export interface FileRoutesById { '/planes/$planId/_detalle': typeof PlanesPlanIdDetalleRouteRouteWithChildren '/planes/$planId/asignaturas': typeof PlanesPlanIdAsignaturasRouteRouteWithChildren '/planes/_lista/nuevo': typeof PlanesListaNuevoRoute + '/planes/$planId/': typeof PlanesPlanIdIndexRoute '/planes/$planId/asignaturas/$asignaturaId': typeof PlanesPlanIdAsignaturasAsignaturaIdRouteRoute '/planes/$planId/asignaturas/_lista': typeof PlanesPlanIdAsignaturasListaRouteRouteWithChildren '/planes/$planId/_detalle/datos': typeof PlanesPlanIdDetalleDatosRoute @@ -218,6 +226,7 @@ export interface FileRouteTypes { | '/planes/$planId' | '/planes/$planId/asignaturas' | '/planes/nuevo' + | '/planes/$planId/' | '/planes/$planId/asignaturas/$asignaturaId' | '/planes/$planId/datos' | '/planes/$planId/documento' @@ -259,6 +268,7 @@ export interface FileRouteTypes { | '/planes/$planId/_detalle' | '/planes/$planId/asignaturas' | '/planes/_lista/nuevo' + | '/planes/$planId/' | '/planes/$planId/asignaturas/$asignaturaId' | '/planes/$planId/asignaturas/_lista' | '/planes/$planId/_detalle/datos' @@ -281,6 +291,7 @@ export interface RootRouteChildren { PlanesPlanesListRouteRoute: typeof PlanesPlanesListRouteRoute PlanesPlanIdDetalleRouteRoute: typeof PlanesPlanIdDetalleRouteRouteWithChildren PlanesPlanIdAsignaturasRouteRoute: typeof PlanesPlanIdAsignaturasRouteRouteWithChildren + PlanesPlanIdIndexRoute: typeof PlanesPlanIdIndexRoute } declare module '@tanstack/react-router' { @@ -327,6 +338,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof PlanesListaRouteRouteImport parentRoute: typeof rootRouteImport } + '/planes/$planId/': { + id: '/planes/$planId/' + path: '/planes/$planId' + fullPath: '/planes/$planId/' + preLoaderRoute: typeof PlanesPlanIdIndexRouteImport + parentRoute: typeof rootRouteImport + } '/planes/_lista/nuevo': { id: '/planes/_lista/nuevo' path: '/nuevo' @@ -510,6 +528,7 @@ const rootRouteChildren: RootRouteChildren = { PlanesPlanIdDetalleRouteRoute: PlanesPlanIdDetalleRouteRouteWithChildren, PlanesPlanIdAsignaturasRouteRoute: PlanesPlanIdAsignaturasRouteRouteWithChildren, + PlanesPlanIdIndexRoute: PlanesPlanIdIndexRoute, } export const routeTree = rootRouteImport ._addFileChildren(rootRouteChildren) diff --git a/src/routes/planes/$planId/_detalle/datos.tsx b/src/routes/planes/$planId/_detalle/datos.tsx index 9e6584f..e071db1 100644 --- a/src/routes/planes/$planId/_detalle/datos.tsx +++ b/src/routes/planes/$planId/_detalle/datos.tsx @@ -73,7 +73,7 @@ function DatosGeneralesPage() { navigate({ to: '/planes/$planId/iaplan', params: { - planId: '1', // o dinámico + planId: planId, // o dinámico }, state: { prefill: descripcion, diff --git a/src/routes/planes/$planId/_detalle/route.tsx b/src/routes/planes/$planId/_detalle/route.tsx index 2d1540e..25c3aeb 100644 --- a/src/routes/planes/$planId/_detalle/route.tsx +++ b/src/routes/planes/$planId/_detalle/route.tsx @@ -5,12 +5,19 @@ import { Clock, Hash, CalendarDays, - Rocket, - BookOpen, - CheckCircle2, + Save, } from 'lucide-react' +import { useState, useEffect } from 'react' import { Badge } from '@/components/ui/badge' +import { Button } from '@/components/ui/button' +import { + ContextMenu, + ContextMenuContent, + ContextMenuItem, + ContextMenuTrigger, +} from '@/components/ui/context-menu' +import { usePlan } from '@/data/hooks/usePlans' export const Route = createFileRoute('/planes/$planId/_detalle')({ component: RouteComponent, @@ -18,10 +25,55 @@ export const Route = createFileRoute('/planes/$planId/_detalle')({ function RouteComponent() { const { planId } = Route.useParams() + const { data } = usePlan(planId) + + // Estados locales para manejar la edición "en vivo" antes de persistir + const [nombrePlan, setNombrePlan] = useState('') + const [nivelPlan, setNivelPlan] = useState('') + const [isDirty, setIsDirty] = useState(false) + + useEffect(() => { + if (data) { + setNombrePlan(data.nombre || '') + setNivelPlan(data.nivel || '') + } + }, [data]) + + const niveles = [ + 'Licenciatura', + 'Maestría', + 'Doctorado', + 'Diplomado', + 'Especialidad', + ] + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + e.preventDefault() // Evita el salto de línea + e.currentTarget.blur() // Quita el foco, lo que dispara el onBlur y "guarda" en el estado + } + } + + const handleSave = () => { + console.log('Guardando en DB...', { nombrePlan, nivelPlan }) + // Aquí iría tu mutation + setIsDirty(false) + } return (
- {/* 1. Header Superior con Sombra (Volver a planes) */} + {/* Botón Flotante de Guardar */} + {isDirty && ( +
+ +
+ )} + {/* 1. Header Superior */}
- {/* 2. Contenido Principal con Padding */}
- {/* Header del Plan y Badges */} + {/* Header del Plan */}
-

- Plan de Estudios 2024 +

+ {nivelPlan} en + setNombrePlan(e.currentTarget.textContent || '')} + className="cursor-text border-b border-transparent decoration-transparent transition-colors outline-none select-text hover:border-slate-300 focus:border-teal-500" + style={{ WebkitTextDecoration: 'none', textDecoration: 'none' }} // Doble seguridad contra subrayados + > + {nombrePlan} +

- Ingeniería en Sistemas Computacionales + {data?.carreras?.facultades?.nombre}{' '} + {data?.carreras?.nombre_corto}

- {/* Badges de la derecha */}
+ {/* + {data?.estados_plan?.etiqueta} + */} - Ingeniería - - - Licenciatura - - - En Revisión + {data?.estados_plan?.etiqueta}
- {/* 3. Cards de Información (Nivel, Duración, etc.) */} -
- } - label="Nivel" - value="Superior" - /> + {/* 3. Cards de Información con Context Menu */} +
+ + + {/* Eliminamos el div extra y aplicamos el estilo directamente al trigger si es necesario, + pero con asChild, la InfoCard será el trigger real */} + } + label="Nivel" + value={nivelPlan} + isEditable + /> + + + {niveles.map((n) => ( + setNivelPlan(n)}> + {n} + + ))} + + + } label="Duración" - value="9 Semestres" + value={`${data?.numero_ciclos || 0} Ciclos`} /> } @@ -86,7 +158,7 @@ function RouteComponent() { } label="Creación" - value="14 ene 2024" + value={data?.creado_en?.split('T')[0]} // Cortamos la fecha para que no sea tan larga />
@@ -117,7 +189,6 @@ function RouteComponent() {
- {/* 5. Contenido del Tab */}
@@ -126,24 +197,37 @@ function RouteComponent() { ) } -// Sub-componente para las tarjetas de información function InfoCard({ icon, label, value, + isEditable, }: { icon: React.ReactNode label: string - value: string + value: string | number | undefined + isEditable?: boolean }) { return ( -
-
{icon}
-
-

+

+
+ {icon} +
+
+ {' '} + {/* min-w-0 es vital para que el truncate funcione en flex */} +

{label}

-

{value}

+

+ {value || '---'} +

) @@ -163,9 +247,7 @@ function Tab({ to={to} params={params} className="border-b-2 border-transparent pb-3 text-sm font-medium text-slate-500 transition-all hover:text-slate-800" - activeProps={{ - className: 'border-teal-600 text-teal-700 font-bold', - }} + activeProps={{ className: 'border-teal-600 text-teal-700 font-bold' }} > {children}