From 09d8392a287345c5e5dc7e8198cee7188ace1fcd Mon Sep 17 00:00:00 2001 From: "Roberto.silva" Date: Fri, 6 Feb 2026 10:29:08 -0600 Subject: [PATCH 1/3] Deshacerse de todos estos query params de la URL fix #80 --- src/routes/planes/$planId/_detalle.tsx | 22 +-- .../planes/$planId/_detalle/historial.tsx | 22 +-- src/routes/planes/$planId/_detalle/index.tsx | 7 +- src/routes/planes/$planId/_detalle/mapa.tsx | 132 +++++++++--------- 4 files changed, 81 insertions(+), 102 deletions(-) diff --git a/src/routes/planes/$planId/_detalle.tsx b/src/routes/planes/$planId/_detalle.tsx index 94f1c29..0dcf508 100644 --- a/src/routes/planes/$planId/_detalle.tsx +++ b/src/routes/planes/$planId/_detalle.tsx @@ -26,8 +26,6 @@ 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), @@ -35,8 +33,6 @@ export const Route = createFileRoute('/planes/$planId/_detalle')({ } catch (e: any) { // PGRST116: The result contains 0 rows if (e?.code === 'PGRST116') { - console.log('not found on', Route.path) - throw notFound() } throw e @@ -85,7 +81,6 @@ function RouteComponent() { } const handleSave = () => { - console.log('Guardando en DB...', { nombrePlan, nivelPlan }) // Aquí iría tu mutation setIsDirty(false) } @@ -221,11 +216,7 @@ function RouteComponent() { Datos Generales - + Mapa Curricular @@ -240,13 +231,7 @@ function RouteComponent() { Documento - + Historial @@ -300,7 +285,6 @@ const InfoCard = forwardRef< function Tab({ to, params, - search, children, }: { to: string @@ -308,12 +292,10 @@ function Tab({ search?: any children: React.ReactNode }) { - console.log(search) return ( ({ - structure: search.structure ?? null, - }), }) const getEventConfig = (tipo: string, campo: string) => { @@ -61,14 +58,23 @@ const getEventConfig = (tipo: string, campo: string) => { function RouteComponent() { const { planId } = Route.useParams() const { data: rawData, isLoading } = usePlanHistorial(planId) - const { structure } = Route.useSearch() - console.log(structure?.vigencia?.title) - console.log(structure) + const [structure, setStructure] = useState(null) + const { data } = usePlan(planId) + console.log('analizando estructura') + + console.log(data?.estructuras_plan?.definicion?.properties) + // console.log(structure) // ESTADOS PARA EL MODAL const [selectedEvent, setSelectedEvent] = useState(null) const [isModalOpen, setIsModalOpen] = useState(false) + useEffect(() => { + if (data?.estructuras_plan?.definicion?.properties) { + setStructure(data.estructuras_plan.definicion.properties) + } + }, [data]) + const historyEvents = useMemo(() => { if (!rawData) return [] return rawData.map((item: any) => { diff --git a/src/routes/planes/$planId/_detalle/index.tsx b/src/routes/planes/$planId/_detalle/index.tsx index d4582e4..86587ae 100644 --- a/src/routes/planes/$planId/_detalle/index.tsx +++ b/src/routes/planes/$planId/_detalle/index.tsx @@ -107,8 +107,6 @@ function DatosGeneralesPage() { setCampos(datosTransformados) } - - console.log(properties) }, [data]) // 3. Manejadores de acciones (Ahora como funciones locales) @@ -166,8 +164,6 @@ function DatosGeneralesPage() { } const handleIARequest = (clave: string) => { - console.log(clave) - navigate({ to: '/planes/$planId/iaplan', params: { @@ -190,9 +186,8 @@ function DatosGeneralesPage() {
- {campos.map((campo, key) => { + {campos.map((campo) => { const isEditing = editingId === campo.id - console.log(campo) return (
({ - ciclo: search.ciclo ?? null, - }), }) function MapaCurricularPage() { const { planId } = Route.useParams() // Idealmente usa el ID de la ruta - const { ciclo } = Route.useSearch() + const { data } = usePlan(planId) + const [ciclo, setCiclo] = useState(0) const [editingLineaId, setEditingLineaId] = useState(null) const [tempNombreLinea, setTempNombreLinea] = useState('') const { mutate: createLinea } = useCreateLinea() const { mutate: updateLineaApi } = useUpdateLinea() const { mutate: deleteLineaApi } = useDeleteLinea() - // 1. Fetch de Datos const { data: asignaturasApi, isLoading: loadingAsig } = usePlanAsignaturas(planId) const { data: lineasApi, isLoading: loadingLineas } = usePlanLineas(planId) - - // 2. Estado Local (Para interactividad) const [asignaturas, setAsignaturas] = useState>([]) const [lineas, setLineas] = useState>([]) const [draggedAsignatura, setDraggedAsignatura] = useState( null, ) const [isEditModalOpen, setIsEditModalOpen] = useState(false) - const [selectedAsignatura, setSelectedAsignatura] = - useState(null) - const [hasAreaComun, setHasAreaComun] = useState(false) const [nombreNuevaLinea, setNombreNuevaLinea] = useState('') // Para el input de nombre personalizado - const { mutate: updateAsignatura, isPending } = useUpdateAsignatura() + const { mutate: updateAsignatura } = useUpdateAsignatura() + const [seriacionValue, setSeriacionValue] = useState('unassigned') + + useEffect(() => { + if (data?.numero_ciclos) { + setCiclo(data.numero_ciclos) + } + }, [data]) const manejarAgregarLinea = (nombre: string) => { const nombreNormalizado = nombre.trim() - - // 1. Validar vacío if (!nombreNormalizado) return - - // 2. Validar duplicados en el estado local (Insensible a mayúsculas/acentos) const nombreBusqueda = nombreNormalizado .toLowerCase() .normalize('NFD') @@ -219,12 +215,9 @@ function MapaCurricularPage() { if (yaExiste) { alert(`La línea "${nombreNormalizado}" ya existe en este plan.`) - return // DETIENE la ejecución aquí, no llega a la mutación + return } - - // 3. Si pasa las validaciones, procedemos con la persistencia const maxOrden = lineas.reduce((max, l) => Math.max(max, l.orden || 0), 0) - createLinea( { nombre: nombreNormalizado, @@ -234,7 +227,6 @@ function MapaCurricularPage() { }, { onSuccess: (nueva) => { - // Sincronización local que ya teníamos const mapeada = { id: nueva.id, nombre: nueva.nombre, @@ -280,7 +272,6 @@ function MapaCurricularPage() { ) }, [lineas]) - // 3. Sincronizar API -> Estado Local useEffect(() => { if (asignaturasApi) setAsignaturas(mapAsignaturasToAsignaturas(asignaturasApi)) @@ -292,20 +283,27 @@ function MapaCurricularPage() { const ciclosTotales = Number(ciclo) const ciclosArray = Array.from({ length: ciclosTotales }, (_, i) => i + 1) - - // Nuevo estado para controlar los datos temporales del modal de edición const [editingData, setEditingData] = useState(null) + const handleDecimalChange = (value: string, max?: number): string | null => { + if (value === '') return '' - // 1. FUNCION DE GUARDAR MODAL + const val = value.replace(',', '.') + const regex = /^\d*\.?\d{0,2}$/ + if (!regex.test(val)) return null + if (max !== undefined) { + const num = Number(val) + if (!isNaN(num) && num > max) { + return max.toFixed(2) + } + } + + return val + } const handleSaveChanges = () => { if (!editingData) return - console.log(asignaturas) - setAsignaturas((prev) => prev.map((m) => (m.id === editingData.id ? { ...editingData } : m)), ) - // setIsEditModalOpen(false) - // Preparamos el patch con la estructura de tu tabla const patch = { nombre: editingData.nombre, codigo: editingData.clave, @@ -332,22 +330,11 @@ function MapaCurricularPage() { }, ) } - - // 2. MODIFICACIÓN: Zona de soltado siempre visible - // Cambiamos la condición: Mostramos la sección si hay asignaturas sin asignar - // O si simplemente queremos tener el "depósito" disponible. const unassignedAsignaturas = asignaturas.filter( (m) => m.ciclo === null || m.lineaCurricularId === null, ) - // --- Lógica de Gestión --- - const agregarLinea = (nombre: string) => { - const nueva = { id: crypto.randomUUID(), nombre, orden: lineas.length + 1 } - setLineas([...lineas, nueva]) - } - const borrarLinea = (id: string) => { - // 1. Opcional: Confirmación de seguridad if ( !confirm( '¿Estás seguro de eliminar esta línea? Las materias asignadas volverán a la bandeja de entrada.', @@ -356,11 +343,8 @@ function MapaCurricularPage() { return } - // 2. Llamada a la API deleteLineaApi(id, { onSuccess: () => { - // 3. Actualización instantánea del estado local - // Primero: Las materias que estaban en esa línea pasan a ser "huérfanas" setAsignaturas((prev) => prev.map((asig) => @@ -369,8 +353,6 @@ function MapaCurricularPage() { : asig, ), ) - - // Segundo: Quitamos la línea del estado setLineas((prev) => prev.filter((l) => l.id !== id)) }, onError: (error) => { @@ -427,8 +409,6 @@ function MapaCurricularPage() { : m, ), ) - - // 2. Persistir en la API const patch = { numero_ciclo: ciclo, linea_plan_id: lineaId, @@ -774,13 +754,17 @@ function MapaCurricularPage() { - setEditingData({ - ...editingData, - creditos: Number(e.target.value), - }) - } + onChange={(e) => { + const val = handleDecimalChange(e.target.value, 10) + if (val !== null) { + setEditingData({ + ...editingData, + creditos: val === '' ? 0 : Number(val), + }) + } + }} />
@@ -790,12 +774,15 @@ function MapaCurricularPage() { - setEditingData({ - ...editingData, - hd: Number(e.target.value), - }) - } + onChange={(e) => { + const val = handleDecimalChange(e.target.value, 10) + if (val !== null) { + setEditingData({ + ...editingData, + hd: Number(e.target.value), + }) + } + }} />
@@ -805,12 +792,15 @@ function MapaCurricularPage() { - setEditingData({ - ...editingData, - hi: Number(e.target.value), - }) - } + onChange={(e) => { + const val = handleDecimalChange(e.target.value, 10) + if (val !== null) { + setEditingData({ + ...editingData, + hi: Number(e.target.value), + }) + } + }} />
@@ -882,23 +872,29 @@ function MapaCurricularPage() { Seriación (Prerrequisitos) setTempNombreLinea(e.target.value)} @@ -572,6 +581,7 @@ function MapaCurricularPage() { } /> ) : ( + // eslint-disable-next-line jsx-a11y/no-static-element-interactions { @@ -727,6 +737,7 @@ function MapaCurricularPage() { Clave setEditingData({ ...editingData, clave: e.target.value }) @@ -738,6 +749,7 @@ function MapaCurricularPage() { Nombre setEditingData({ ...editingData, nombre: e.target.value }) @@ -775,7 +787,7 @@ function MapaCurricularPage() { type="number" value={editingData.hd} onChange={(e) => { - const val = handleDecimalChange(e.target.value, 10) + const val = handleIntegerChange(e.target.value) if (val !== null) { setEditingData({ ...editingData, @@ -793,7 +805,7 @@ function MapaCurricularPage() { type="number" value={editingData.hi} onChange={(e) => { - const val = handleDecimalChange(e.target.value, 10) + const val = handleIntegerChange(e.target.value) if (val !== null) { setEditingData({ ...editingData, From 581dc566bcc13455a956dddcf2b8380181897e5d Mon Sep 17 00:00:00 2001 From: "Roberto.silva" Date: Fri, 6 Feb 2026 15:54:40 -0600 Subject: [PATCH 3/3] Que no sean INPUTS fix #72 --- src/routes/planes/$planId/_detalle/mapa.tsx | 106 +++++++++++++++----- 1 file changed, 79 insertions(+), 27 deletions(-) diff --git a/src/routes/planes/$planId/_detalle/mapa.tsx b/src/routes/planes/$planId/_detalle/mapa.tsx index 8ee42f5..287ac91 100644 --- a/src/routes/planes/$planId/_detalle/mapa.tsx +++ b/src/routes/planes/$planId/_detalle/mapa.tsx @@ -1,4 +1,4 @@ -/* eslint-disable jsx-a11y/click-events-have-key-events */ +/* eslint-disable jsx-a11y/no-static-element-interactions */ /* eslint-disable jsx-a11y/label-has-associated-control */ import { createFileRoute } from '@tanstack/react-router' import { @@ -7,6 +7,7 @@ import { AlertTriangle, GripVertical, Trash2, + Pencil, } from 'lucide-react' import { useMemo, useState, useEffect } from 'react' @@ -239,8 +240,13 @@ function MapaCurricularPage() { }, ) } - const guardarEdicionLinea = (id: string) => { - if (!tempNombreLinea.trim()) { + const guardarEdicionLinea = (id: string, nuevoNombre?: string) => { + // Usamos el nombre que viene por parámetro o el del estado como fallback + const nombreAFijar = ( + nuevoNombre !== undefined ? nuevoNombre : tempNombreLinea + ).trim() + + if (!nombreAFijar) { setEditingLineaId(null) return } @@ -248,11 +254,10 @@ function MapaCurricularPage() { updateLineaApi( { lineaId: id, - patch: { nombre: tempNombreLinea.trim() }, + patch: { nombre: nombreAFijar }, }, { onSuccess: (lineaActualizada) => { - // ACTUALIZACIÓN MANUAL DEL ESTADO LOCAL setLineas((prev) => prev.map((l) => l.id === id ? { ...l, nombre: lineaActualizada.nombre } : l, @@ -261,6 +266,10 @@ function MapaCurricularPage() { setEditingLineaId(null) setTempNombreLinea('') }, + onError: (err) => { + console.error('Error al actualizar linea:', err) + // Opcional: revertir cambios o avisar al usuario + }, }, ) } @@ -454,6 +463,33 @@ function MapaCurricularPage() { [asignaturas], ) + const handleKeyDownLinea = ( + e: React.KeyboardEvent, + id: string, + ) => { + if (e.key === 'Enter') { + e.preventDefault() + e.currentTarget.blur() + } + } + + const handleBlurLinea = ( + e: React.FocusEvent, + id: string, + ) => { + const nuevoNombre = e.currentTarget.textContent?.trim() || '' + + // Buscamos la línea original para comparar + const lineaOriginal = lineas.find((l) => l.id === id) + + if (nuevoNombre !== lineaOriginal?.nombre) { + // IMPORTANTE: Pasamos nuevoNombre directamente + guardarEdicionLinea(id, nuevoNombre) + } else { + setEditingLineaId(null) + } + } + if (loadingAsig || loadingLineas) return
Cargando mapa curricular...
@@ -568,36 +604,52 @@ function MapaCurricularPage() { }} >
- {editingLineaId === linea.id ? ( - setTempNombreLinea(e.target.value)} - onBlur={() => guardarEdicionLinea(linea.id)} - onKeyDown={(e) => - e.key === 'Enter' && guardarEdicionLinea(linea.id) - } - /> - ) : ( - // eslint-disable-next-line jsx-a11y/no-static-element-interactions +
handleKeyDownLinea(e, linea.id)} + onBlur={(e) => handleBlurLinea(e, linea.id)} onClick={() => { - setEditingLineaId(linea.id) - setTempNombreLinea(linea.nombre) + if (editingLineaId !== linea.id) { + setEditingLineaId(linea.id) + setTempNombreLinea(linea.nombre) + } }} + className={`block w-full text-xs font-bold break-words outline-none ${ + editingLineaId === linea.id + ? 'cursor-text border-b border-teal-500/50 pb-1' + : 'cursor-pointer' + }`} > {linea.nombre} - )} +
- borrarLinea(linea.id)} // Aquí también podrías añadir una mutación delete - /> +
+ {/* Botón de edición que aparece en hover o si está editando */} + + + borrarLinea(linea.id)} + /> +
{ciclosArray.map((ciclo) => (