diff --git a/src/components/asignaturas/detalle/AsignaturaDetailPage.tsx b/src/components/asignaturas/detalle/AsignaturaDetailPage.tsx index cf97662..66fb691 100644 --- a/src/components/asignaturas/detalle/AsignaturaDetailPage.tsx +++ b/src/components/asignaturas/detalle/AsignaturaDetailPage.tsx @@ -128,8 +128,6 @@ export default function AsignaturaDetailPage() { } /* ---------- sincronizar API ---------- */ useEffect(() => { - console.log(requisitosFormateados) - if (asignaturaApi) setAsignatura(asignaturaApi) }, [asignaturaApi, requisitosFormateados]) @@ -182,7 +180,6 @@ function DatosGenerales({ const criteriosEvaluacion: Array = useMemo(() => { const raw = (data as any)?.criterios_de_evaluacion - console.log(raw) if (!Array.isArray(raw)) return [] @@ -414,8 +411,7 @@ function InfoCard({ useEffect(() => { setData(initialContent) setTempText(initialContent) - console.log(data) - console.log(initialContent) + if (type === 'evaluation') { const raw = Array.isArray(initialContent) ? initialContent : [] const rows: Array = raw @@ -457,10 +453,6 @@ function InfoCard({ }, [highlightToken]) const handleSave = () => { - console.log('clave, valor:', clave, String(tempText ?? '')) - console.log(clave) - console.log(tempText) - if (type === 'evaluation') { const cleaned: Array = [] for (const r of evalRows) { @@ -491,8 +483,6 @@ function InfoCard({ return } if (type === 'requirements') { - console.log('entre aqui ') - // Si tempText es un array y tiene elementos, tomamos el ID del primero // Si es "none" o está vacío, mandamos null (para limpiar la seriación) const prerequisiteId = @@ -519,8 +509,6 @@ function InfoCard({ } const handleIARequest = (campoClave: string) => { - console.log(campoClave) - let targetClave = campoClave if (type === 'evaluation' && !targetClave) { targetClave = 'criterios_de_evaluacion' @@ -529,8 +517,6 @@ function InfoCard({ if (targetClave === 'contenido') { targetClave = 'contenido_tematico' } - console.log(targetClave) - console.log(asignaturaId) navigate({ to: '/planes/$planId/asignaturas/$asignaturaId/iaasignatura', @@ -652,18 +638,12 @@ function InfoCard({ : 'none' } onValueChange={(val) => { - console.log(availableSubjects) - const selected = availableSubjects?.find( (s) => s.id === val, ) if (val === 'none' || !selected) { - console.log('guardando') - setTempText([]) } else { - console.log('hola') - setTempText([ { id: selected.id, @@ -690,6 +670,9 @@ function InfoCard({ value={asig.id} className="max-w-[300px] sm:max-w-[500px]" > + + [C{asig.numero_ciclo}] + {' '} {asig.codigo} - {asig.nombre} diff --git a/src/components/asignaturas/detalle/mapa/AlertaConflicto.tsx b/src/components/asignaturas/detalle/mapa/AlertaConflicto.tsx index b529637..ce0d7c5 100644 --- a/src/components/asignaturas/detalle/mapa/AlertaConflicto.tsx +++ b/src/components/asignaturas/detalle/mapa/AlertaConflicto.tsx @@ -36,7 +36,8 @@ export const AlertaConflicto = ({ ))}

- ¿Deseas ignorar la regla y moverla de todos modos? + ¿Deseas ignorar la regla y moverla de todos modos (Esto eliminará la + seriación)?

) @@ -62,13 +63,8 @@ export const AlertaConflicto = ({
- + + {/* Radix automáticamente llamará a onOpenChange(false) al hacer clic aquí */} diff --git a/src/routes/planes/$planId/_detalle/mapa.tsx b/src/routes/planes/$planId/_detalle/mapa.tsx index bb98b87..a253096 100644 --- a/src/routes/planes/$planId/_detalle/mapa.tsx +++ b/src/routes/planes/$planId/_detalle/mapa.tsx @@ -6,7 +6,6 @@ import { useMemo, useState, useEffect, Fragment } from 'react' import type { TipoAsignatura } from '@/data' import type { Asignatura, LineaCurricular } from '@/types/plan' -import type { TablesUpdate } from '@/types/supabase' import { AlertaConflicto } from '@/components/asignaturas/detalle/mapa/AlertaConflicto' import { Badge } from '@/components/ui/badge' @@ -404,15 +403,19 @@ function MapaCurricularPage() { asignatura: Asignatura, nuevoCiclo: number, ) => { + // 1. Esperamos la interacción del usuario const acepto = await validarConInterrupcion(asignatura.id, nuevoCiclo) - if (!acepto) { - setConfirmState(null) - return + // 2. Limpiamos el estado del modal ANTES de seguir + setConfirmState(null) + + // 3. Verificamos la respuesta + if (acepto === false) { + return // Detenemos la ejecución aquí } + // 4. Solo si acepto es true, ejecutamos la API const patch = { numero_ciclo: nuevoCiclo } - updateAsignatura( { asignaturaId: asignatura.id, patch: patch as any }, { @@ -422,12 +425,9 @@ function MapaCurricularPage() { m.id === asignatura.id ? { ...m, ciclo: nuevoCiclo } : m, ), ) - setConfirmState(null) // Cerramos el modal de Radix al tener éxito }, onError: (err) => { - // En lugar de alert nativo, podrías usar un toast aquí console.error('Error al actualizar:', err) - setConfirmState(null) }, }, ) @@ -549,48 +549,61 @@ function MapaCurricularPage() { return val } - const handleSaveChanges = () => { - if (!editingData) return - setAsignaturas((prev) => - prev.map((m) => (m.id === editingData.id ? { ...editingData } : m)), - ) - type AsignaturaPatch = { - codigo?: TablesUpdate<'asignaturas'>['codigo'] - nombre?: TablesUpdate<'asignaturas'>['nombre'] - tipo?: TablesUpdate<'asignaturas'>['tipo'] - creditos?: TablesUpdate<'asignaturas'>['creditos'] - horas_academicas?: TablesUpdate<'asignaturas'>['horas_academicas'] - horas_independientes?: TablesUpdate<'asignaturas'>['horas_independientes'] - numero_ciclo?: TablesUpdate<'asignaturas'>['numero_ciclo'] - linea_plan_id?: TablesUpdate<'asignaturas'>['linea_plan_id'] - prerrequisito_asignatura_id?: string | null + + const procesarCambioAsignatura = async ( + asignaturaId: string, + nuevosDatos: Partial, + ) => { + const asignaturaOriginal = asignaturas.find((a) => a.id === asignaturaId) + if (!asignaturaOriginal) return + + // ¿Cambió el ciclo? Si es así, validamos seriación + if ( + nuevosDatos.ciclo !== undefined && + nuevosDatos.ciclo !== asignaturaOriginal.ciclo + ) { + const acepto = await validarConInterrupcion( + asignaturaId, + nuevosDatos.ciclo, + ) + setConfirmState(null) + if (!acepto) return // El usuario canceló, no guardamos nada } - const patch: Partial = { - nombre: editingData.nombre, - codigo: editingData.clave, - creditos: editingData.creditos, - horas_academicas: editingData.hd, - horas_independientes: editingData.hi, - numero_ciclo: editingData.ciclo, - linea_plan_id: editingData.lineaCurricularId, - prerrequisito_asignatura_id: editingData.prerrequisito_asignatura_id, - tipo: editingData.tipo.toUpperCase() as TipoAsignatura, // Asegurar que coincida con el ENUM (OBLIGATORIA/OPTATIVA) + + // Si llegamos aquí, o no cambió el ciclo o el usuario aceptó el conflicto + const patch = { + nombre: nuevosDatos.nombre ?? asignaturaOriginal.nombre, + codigo: nuevosDatos.clave ?? asignaturaOriginal.clave, + numero_ciclo: nuevosDatos.ciclo, + linea_plan_id: nuevosDatos.lineaCurricularId, + creditos: nuevosDatos.creditos, + horas_academicas: nuevosDatos.hd, + horas_independientes: nuevosDatos.hi, + prerrequisito_asignatura_id: nuevosDatos.prerrequisito_asignatura_id, + tipo: nuevosDatos.tipo?.toUpperCase() as TipoAsignatura, } updateAsignatura( - { asignaturaId: editingData.id, patch: patch as any }, + { asignaturaId, patch: patch as any }, { onSuccess: () => { - setIsEditModalOpen(false) - // Opcional: Mostrar un toast de éxito - }, - onError: (error) => { - console.error('Error al guardar:', error) - alert('Hubo un error al guardar los cambios.') + setAsignaturas((prev) => + prev.map((m) => + m.id === asignaturaId ? { ...m, ...nuevosDatos } : m, + ), + ) + setIsEditModalOpen(false) // Cerramos el modal si estaba abierto }, + onError: (err) => console.error('Error al guardar:', err), }, ) } + const handleSaveChanges = () => { + if (!editingData) return + + // Llamamos a la lógica centralizada que incluye la alerta + procesarCambioAsignatura(editingData.id, editingData) + } const unassignedAsignaturas = asignaturas.filter( (m) => m.ciclo === null || m.lineaCurricularId === null, ) @@ -662,41 +675,13 @@ function MapaCurricularPage() { lineaId: string | null, ) => { e.preventDefault() - if (!draggedAsignatura) return - // Buscamos la asignatura completa para tener sus datos - const asignatura = asignaturas.find((a) => a.id === draggedAsignatura) - if (!asignatura) return - - // SI EL CAMBIO ES DE CICLO (y no solo de línea o a la bandeja) - // Ejecutamos la validación segura - if (cicloDestino !== null && cicloDestino !== asignatura.ciclo) { - // Llamamos a la función que agregaste - // IMPORTANTE: Asegúrate que handleCambioCicloSeguro esté definida ANTES o sea accesible - await handleCambioCicloSeguro(asignatura, cicloDestino) - - // Si la validación interna de handleCambioCicloSeguro falla o el usuario cancela, - // la ejecución se detiene dentro de esa función. - } else { - // CASO B: Es un movimiento a la bandeja (null) o cambio de línea en el mismo ciclo - // Mantenemos la lógica simple de actualización - setAsignaturas((prev) => - prev.map((m) => - m.id === draggedAsignatura - ? { ...m, ciclo: cicloDestino, lineaCurricularId: lineaId } - : m, - ), - ) - - updateAsignatura( - { - asignaturaId: draggedAsignatura, - patch: { numero_ciclo: cicloDestino, linea_plan_id: lineaId }, - }, - { onError: (err) => console.error('Error al mover:', err) }, - ) - } + // Solo disparamos la lógica si realmente hay un cambio de posición + procesarCambioAsignatura(draggedAsignatura, { + ciclo: cicloDestino, + lineaCurricularId: lineaId, + }) setDraggedAsignatura(null) } diff --git a/src/routes/planes/$planId/asignaturas/$asignaturaId/route.tsx b/src/routes/planes/$planId/asignaturas/$asignaturaId/route.tsx index 48ec8a4..2aa9789 100644 --- a/src/routes/planes/$planId/asignaturas/$asignaturaId/route.tsx +++ b/src/routes/planes/$planId/asignaturas/$asignaturaId/route.tsx @@ -9,9 +9,10 @@ import { import { ArrowLeft, GraduationCap } from 'lucide-react' import { useEffect, useState } from 'react' +import { AlertaConflicto } from '@/components/asignaturas/detalle/mapa/AlertaConflicto' import { Badge } from '@/components/ui/badge' import { lateralConfetti } from '@/components/ui/lateral-confetti' -import { useSubject, useUpdateAsignatura } from '@/data' +import { usePlanAsignaturas, useSubject, useUpdateAsignatura } from '@/data' export const Route = createFileRoute( '/planes/$planId/asignaturas/$asignaturaId', @@ -73,7 +74,44 @@ function AsignaturaLayout() { }) const { data: asignaturaApi, isLoading: loadingAsig } = useSubject(asignaturaId) - // 1. Asegúrate de tener estos estados en tu componente principal + const { data: todasLasAsignaturas } = usePlanAsignaturas(planId) + const [confirmState, setConfirmState] = useState<{ + isOpen: boolean + resolve: (value: boolean) => void + mensaje: string + } | null>(null) + const validarConInterrupcion = async ( + nuevoCiclo: number, + ): Promise => { + if (!todasLasAsignaturas || !asignaturaApi) return true + + const materiasConflicto = todasLasAsignaturas.filter((a) => { + const esPrerrequisitoConflictivo = + asignaturaApi.prerrequisito_asignatura_id === a.id && + (a.numero_ciclo ?? 0) >= nuevoCiclo + + const esDependienteConflictiva = + a.prerrequisito_asignatura_id === asignaturaApi.id && + (a.numero_ciclo ?? 0) <= nuevoCiclo + + return esPrerrequisitoConflictivo || esDependienteConflictiva + }) + + if (materiasConflicto.length === 0) return true + + const listaNombres = materiasConflicto.map((m) => m.nombre) + + return new Promise((resolve) => { + setConfirmState({ + isOpen: true, + resolve, + mensaje: JSON.stringify({ + main: `Mover "${asignaturaApi.nombre}" al ciclo ${nuevoCiclo} genera conflictos con:`, + materias: listaNombres, + }), + }) + }) + } const updateAsignatura = useUpdateAsignatura() @@ -97,16 +135,30 @@ function AsignaturaLayout() { } }, [asignaturaApi]) - const handleUpdateHeader = (key: string, value: string | number) => { + const handleUpdateHeader = async (key: string, value: string | number) => { + // 1. Si es ciclo, validamos antes de hacer nada + if (key === 'ciclo') { + const nuevoCiclo = Number(value) + const acepto = await validarConInterrupcion(nuevoCiclo) + + setConfirmState(null) // Cerramos el modal tras la respuesta + + if (!acepto) { + // Revertimos el estado local al valor de la API si cancela + setHeaderData((prev) => ({ + ...prev, + ciclo: asignaturaApi?.numero_ciclo ?? 0, + })) + return + } + } + + // 2. Si no es ciclo o si aceptó el conflicto, procedemos const newData = { ...headerData, [key]: value } setHeaderData(newData) const patch: Record = - key === 'ciclo' - ? { numero_ciclo: value } - : { - [key]: value, - } + key === 'ciclo' ? { numero_ciclo: value } : { [key]: value } updateAsignatura.mutate({ asignaturaId, @@ -166,11 +218,6 @@ function AsignaturaLayout() { onSave={(val) => handleUpdateHeader('nombre', val)} /> - { - // console.log(headerData), - - console.log(asignaturaApi.planes_estudio?.nombre) - }
@@ -213,6 +260,21 @@ function AsignaturaLayout() {
+ {confirmState && ( + { + if (!open) { + confirmState.resolve(false) + setConfirmState(null) + } + }} + onConfirm={() => confirmState.resolve(true)} + titulo="Conflicto de Seriación" + descripcion={confirmState.mensaje} + /> + )} + {/* TABS */}