import { createFileRoute, Outlet, Link, notFound } from '@tanstack/react-router' import { ChevronLeft, GraduationCap, Clock, Hash, CalendarDays, Save, } from 'lucide-react' import { useState, useEffect, forwardRef } from 'react' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu' import { NotFoundPage } from '@/components/ui/NotFoundPage' import { Skeleton } from '@/components/ui/skeleton' import { plans_get } from '@/data/api/plans.api' import { usePlan } from '@/data/hooks/usePlans' import { qk } from '@/data/query/keys' export const Route = createFileRoute('/planes/$planId/_detalle')({ loader: async ({ context: { queryClient }, params: { planId } }) => { try { console.log('loader') await queryClient.ensureQueryData({ queryKey: qk.plan(planId), queryFn: () => plans_get(planId), }) } catch (e: any) { // PGRST116: The result contains 0 rows if (e?.code === 'PGRST116') { throw notFound() } throw e } }, notFoundComponent: () => { return ( ) }, component: RouteComponent, }) function RouteComponent() { const { planId } = Route.useParams() const { data, isLoading } = 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 (
{/* Botón Flotante de Guardar */} {isDirty && (
)} {/* 1. Header Superior */}
Volver a planes
{/* Header del Plan */} {isLoading ? ( /* ===== SKELETON ===== */
{Array.from({ length: 6 }).map((_, i) => ( ))}
) : ( <>

{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}

{data?.carreras?.facultades?.nombre}{' '} {data?.carreras?.nombre_corto}

{/* {data?.estados_plan?.etiqueta} */} {data?.estados_plan?.etiqueta}
)} {/* 3. Cards de Información con Context Menu */}
} label="Nivel" value={nivelPlan} isEditable /> {niveles.map((n) => ( { setNivelPlan(n) setIsDirty(true) }} > {n} ))} } label="Duración" value={`${data?.numero_ciclos || 0} Ciclos`} /> } label="Créditos" value="320" /> } label="Creación" value={data?.creado_en?.split('T')[0]} // Cortamos la fecha para que no sea tan larga />
{/* 4. Navegación de Tabs */}
) } const InfoCard = forwardRef< HTMLDivElement, { icon: React.ReactNode label: string value: string | number | undefined isEditable?: boolean } & React.HTMLAttributes >(function InfoCard( { icon, label, value, isEditable, className, ...props }, ref, ) { return (
{icon}

{label}

{value || '---'}

) }) function Tab({ to, params, children, }: { to: string params?: any children: React.ReactNode }) { return ( {children} ) } function DatosGeneralesSkeleton() { return (
{/* Header */}
{/* Content */}
) }