import { createFileRoute, Outlet, Link, notFound } from '@tanstack/react-router' import { ChevronLeft, GraduationCap, Clock, Hash, CalendarDays, } from 'lucide-react' import { useState, useEffect, forwardRef } from 'react' import { Badge } from '@/components/ui/badge' 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, useUpdatePlanFields } from '@/data/hooks/usePlans' import { qk } from '@/data/query/keys' export const Route = createFileRoute('/planes/$planId/_detalle')({ loader: async ({ context: { queryClient }, params: { planId } }) => { try { 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) const { mutate } = useUpdatePlanFields() // 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 persistChange = (patch: any) => { mutate({ planId, patch }) } const MAX_CHARACTERS = 200 const handleKeyDown = (e: React.KeyboardEvent) => { // 1. Permitir teclas de control (Borrar, flechas, etc.) siempre const isControlKey = e.key === 'Backspace' || e.key === 'Delete' || e.key.includes('Arrow') || e.metaKey || e.ctrlKey if (e.key === 'Enter') { e.preventDefault() e.currentTarget.blur() return } // 2. Bloquear si excede los 200 caracteres y no es una tecla de control const currentText = e.currentTarget.textContent || '' if (currentText.length >= MAX_CHARACTERS && !isControlKey) { e.preventDefault() } } const handlePaste = (e: React.ClipboardEvent) => { e.preventDefault() const text = e.clipboardData.getData('text/plain') const currentText = e.currentTarget.textContent || '' // Calcular cuánto espacio queda const remainingSpace = MAX_CHARACTERS - currentText.length if (remainingSpace > 0) { const slicedText = text.slice(0, remainingSpace) document.execCommand('insertText', false, slicedText) } } return ( {/* 1. Header Superior */} Volver a planes {/* 2. Header del Plan */} {isLoading ? ( /* ===== SKELETON ===== */ {Array.from({ length: 6 }).map((_, i) => ( ))} ) : ( {/* El prefijo "Nivel en" lo mantenemos simple */} {nivelPlan} en { const nuevoNombre = e.currentTarget.textContent?.trim() || '' setNombrePlan(nuevoNombre) if (nuevoNombre !== data?.nombre) { mutate({ planId, patch: { nombre: nuevoNombre } }) } }} // Clases añadidas: break-words y whitespace-pre-wrap para el wrap className="block w-full cursor-text border-b border-transparent break-words whitespace-pre-wrap transition-colors outline-none select-text hover:border-slate-300 focus:border-teal-500 sm:inline-block sm:w-auto" style={{ textDecoration: 'none' }} > {nombrePlan} {data?.carreras?.facultades?.nombre}{' '} {data?.carreras?.nombre_corto} {data?.estados_plan?.etiqueta} )} {/* 3. Cards de Información */} } label="Nivel" value={nivelPlan} isEditable /> {niveles.map((n) => ( { setNivelPlan(n) if (n !== data?.nivel) { mutate({ planId, patch: { nivel: n } }) } }} > {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]} /> {/* 4. Navegación de Tabs */} Datos Generales Mapa Curricular Asignaturas Flujo y Estados IA del Plan Documento Historial ) } 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 search?: any children: React.ReactNode }) { return ( {children} ) } function DatosGeneralesSkeleton() { return ( {/* Header */} {/* Content */} ) }
{data?.carreras?.facultades?.nombre}{' '} {data?.carreras?.nombre_corto}
{label}
{value || '---'}