diff --git a/src/routes/planes/$planId/_detalle.tsx b/src/routes/planes/$planId/_detalle.tsx index 2f6b37c..9717dda 100644 --- a/src/routes/planes/$planId/_detalle.tsx +++ b/src/routes/planes/$planId/_detalle.tsx @@ -24,8 +24,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), @@ -33,8 +31,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 @@ -80,31 +76,43 @@ function RouteComponent() { 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() // Esto disparará el onBlur automáticamente + 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 handleBlurNombre = (e: React.FocusEvent) => { - const nuevoNombre = e.currentTarget.textContent || '' - setNombrePlan(nuevoNombre) + const handlePaste = (e: React.ClipboardEvent) => { + e.preventDefault() + const text = e.clipboardData.getData('text/plain') + const currentText = e.currentTarget.textContent || '' - // Solo guardamos si el valor es realmente distinto al de la base de datos - if (nuevoNombre !== data?.nombre) { - persistChange({ nombre: nuevoNombre }) + // 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) } } - - const handleSelectNivel = (n: string) => { - setNivelPlan(n) - // Guardamos inmediatamente al seleccionar - if (n !== data?.nivel) { - persistChange({ nivel: n }) - } - } - return (
{/* 1. Header Superior */} @@ -131,8 +139,9 @@ function RouteComponent() { ) : (
-

- {nivelPlan} en +

+ {/* El prefijo "Nivel en" lo mantenemos simple */} + {nivelPlan} en { - const nuevoNombre = e.currentTarget.textContent || '' + const nuevoNombre = + e.currentTarget.textContent?.trim() || '' setNombrePlan(nuevoNombre) if (nuevoNombre !== data?.nombre) { mutate({ planId, patch: { nombre: nuevoNombre } }) } }} - className="cursor-text border-b border-transparent transition-colors outline-none select-text hover:border-slate-300 focus:border-teal-500" + // 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} @@ -219,11 +231,7 @@ function RouteComponent() { Datos Generales - + Mapa Curricular @@ -238,13 +246,7 @@ function RouteComponent() { Documento - + Historial @@ -298,7 +300,6 @@ const InfoCard = forwardRef< function Tab({ to, params, - search, children, }: { to: string @@ -306,12 +307,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 1a8e457..38e64d2 100644 --- a/src/routes/planes/$planId/_detalle/index.tsx +++ b/src/routes/planes/$planId/_detalle/index.tsx @@ -93,7 +93,6 @@ function DatosGeneralesPage() { requerido: true, - // 👇 TIPO DE CAMPO tipo: Array.isArray(schema?.enum) ? 'select' : schema?.type === 'number' @@ -107,8 +106,6 @@ function DatosGeneralesPage() { setCampos(datosTransformados) } - - console.log(properties) }, [data]) // 3. Manejadores de acciones (Ahora como funciones locales) @@ -220,8 +217,6 @@ function DatosGeneralesPage() { } const handleIARequest = (clave: string) => { - console.log(clave) - navigate({ to: '/planes/$planId/iaplan', params: { @@ -244,9 +239,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 +216,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 +228,6 @@ function MapaCurricularPage() { }, { onSuccess: (nueva) => { - // Sincronización local que ya teníamos const mapeada = { id: nueva.id, nombre: nueva.nombre, @@ -247,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 } @@ -256,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, @@ -269,6 +266,10 @@ function MapaCurricularPage() { setEditingLineaId(null) setTempNombreLinea('') }, + onError: (err) => { + console.error('Error al actualizar linea:', err) + // Opcional: revertir cambios o avisar al usuario + }, }, ) } @@ -280,7 +281,6 @@ function MapaCurricularPage() { ) }, [lineas]) - // 3. Sincronizar API -> Estado Local useEffect(() => { if (asignaturasApi) setAsignaturas(mapAsignaturasToAsignaturas(asignaturasApi)) @@ -292,20 +292,37 @@ 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 handleIntegerChange = (value: string) => { + if (value === '') return value - // 1. FUNCION DE GUARDAR MODAL + // Solo números, máximo 3 cifras + const regex = /^\d{1,3}$/ + + if (!regex.test(value)) return null + + return value + } + const handleDecimalChange = (value: string, max?: number): string | null => { + if (value === '') return '' + + 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 +349,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 +362,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 +372,6 @@ function MapaCurricularPage() { : asig, ), ) - - // Segundo: Quitamos la línea del estado setLineas((prev) => prev.filter((l) => l.id !== id)) }, onError: (error) => { @@ -427,8 +428,6 @@ function MapaCurricularPage() { : m, ), ) - - // 2. Persistir en la API const patch = { numero_ciclo: ciclo, linea_plan_id: lineaId, @@ -464,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...
@@ -578,36 +604,52 @@ function MapaCurricularPage() { }} >
- {editingLineaId === linea.id ? ( - setTempNombreLinea(e.target.value)} - onBlur={() => guardarEdicionLinea(linea.id)} - onKeyDown={(e) => - e.key === 'Enter' && guardarEdicionLinea(linea.id) - } - /> - ) : ( +
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) => ( @@ -747,6 +789,7 @@ function MapaCurricularPage() { Clave setEditingData({ ...editingData, clave: e.target.value }) @@ -758,6 +801,7 @@ function MapaCurricularPage() { Nombre setEditingData({ ...editingData, nombre: e.target.value }) @@ -774,13 +818,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 +838,15 @@ function MapaCurricularPage() { - setEditingData({ - ...editingData, - hd: Number(e.target.value), - }) - } + onChange={(e) => { + const val = handleIntegerChange(e.target.value) + if (val !== null) { + setEditingData({ + ...editingData, + hd: Number(e.target.value), + }) + } + }} />
@@ -805,12 +856,15 @@ function MapaCurricularPage() { - setEditingData({ - ...editingData, - hi: Number(e.target.value), - }) - } + onChange={(e) => { + const val = handleIntegerChange(e.target.value) + if (val !== null) { + setEditingData({ + ...editingData, + hi: Number(e.target.value), + }) + } + }} />
@@ -882,23 +936,29 @@ function MapaCurricularPage() { Seriación (Prerrequisitos)