From 0e9648d61a0e1de742908d075c202e9cbb5cdc8d Mon Sep 17 00:00:00 2001 From: "Roberto.silva" Date: Wed, 4 Feb 2026 14:29:46 -0600 Subject: [PATCH] =?UTF-8?q?En=20el=20mapa=20curricular=20editar=20los=20no?= =?UTF-8?q?mbres=20de=20las=20l=C3=ADneas=20curriculares=20fix=20#57?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/data/api/subjects.api.ts | 49 ++++++ src/data/hooks/usePlans.ts | 13 ++ src/data/hooks/useSubjects.ts | 27 ++++ src/routes/planes/$planId/_detalle/mapa.tsx | 167 ++++++++++++++------ 4 files changed, 212 insertions(+), 44 deletions(-) diff --git a/src/data/api/subjects.api.ts b/src/data/api/subjects.api.ts index a2d28ef..f81cd8a 100644 --- a/src/data/api/subjects.api.ts +++ b/src/data/api/subjects.api.ts @@ -243,3 +243,52 @@ export async function asignaturas_update( throwIfError(error) return requireData(data, 'No se pudo actualizar la asignatura.') } + +// Insertar una nueva línea +export async function lineas_insert(linea: { + nombre: string + plan_estudio_id: string + orden: number + area?: string +}) { + const supabase = supabaseBrowser() + const { data, error } = await supabase + .from('lineas_plan') // Asegúrate que el nombre de la tabla sea correcto + .insert([linea]) + .select() + .single() + + if (error) throw error + return data +} + +// Actualizar una línea existente +export async function lineas_update( + lineaId: string, + patch: { nombre?: string; orden?: number; area?: string }, +) { + const supabase = supabaseBrowser() + const { data, error } = await supabase + .from('lineas_plan') + .update(patch) + .eq('id', lineaId) + .select() + .single() + + if (error) throw error + return data +} + +export async function lineas_delete(lineaId: string) { + const supabase = supabaseBrowser() + + // Nota: Si configuraste "ON DELETE SET NULL" en tu base de datos, + // las asignaturas se desvincularán solas. Si no, Supabase podría dar error. + const { error } = await supabase + .from('lineas_plan') + .delete() + .eq('id', lineaId) + + if (error) throw error + return lineaId +} diff --git a/src/data/hooks/usePlans.ts b/src/data/hooks/usePlans.ts index 1625ed6..6e7e7d1 100644 --- a/src/data/hooks/usePlans.ts +++ b/src/data/hooks/usePlans.ts @@ -23,6 +23,7 @@ import { plans_update_fields, plans_update_map, } from '../api/plans.api' +import { lineas_delete } from '../api/subjects.api' import { qk } from '../query/keys' import type { @@ -257,3 +258,15 @@ export function useGeneratePlanDocumento() { }, }) } + +export function useDeleteLinea() { + const qc = useQueryClient() + return useMutation({ + mutationFn: lineas_delete, + onSuccess: (idEliminado) => { + // Invalidamos para que las materias y líneas se refresquen + qc.invalidateQueries({ queryKey: ['plan_lineas'] }) + qc.invalidateQueries({ queryKey: ['plan_asignaturas'] }) + }, + }) +} diff --git a/src/data/hooks/useSubjects.ts b/src/data/hooks/useSubjects.ts index ff4d38c..1f4631b 100644 --- a/src/data/hooks/useSubjects.ts +++ b/src/data/hooks/useSubjects.ts @@ -3,6 +3,8 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { ai_generate_subject, asignaturas_update, + lineas_insert, + lineas_update, subjects_bibliografia_list, subjects_clone_from_existing, subjects_create_manual, @@ -222,3 +224,28 @@ export function useUpdateAsignatura() { }, }) } + +export function useCreateLinea() { + const qc = useQueryClient() + return useMutation({ + mutationFn: lineas_insert, + onSuccess: (nuevaLinea) => { + qc.invalidateQueries({ + queryKey: ['plan_lineas', nuevaLinea.plan_estudio_id], + }) + }, + }) +} + +export function useUpdateLinea() { + const qc = useQueryClient() + return useMutation({ + mutationFn: (vars: { lineaId: string; patch: any }) => + lineas_update(vars.lineaId, vars.patch), + onSuccess: (updated) => { + qc.invalidateQueries({ + queryKey: ['plan_lineas', updated.plan_estudio_id], + }) + }, + }) +} diff --git a/src/routes/planes/$planId/_detalle/mapa.tsx b/src/routes/planes/$planId/_detalle/mapa.tsx index 3241506..6eaafd4 100644 --- a/src/routes/planes/$planId/_detalle/mapa.tsx +++ b/src/routes/planes/$planId/_detalle/mapa.tsx @@ -1,3 +1,4 @@ +/* eslint-disable jsx-a11y/click-events-have-key-events */ /* eslint-disable jsx-a11y/label-has-associated-control */ import { createFileRoute } from '@tanstack/react-router' import { @@ -33,7 +34,14 @@ import { SelectTrigger, SelectValue, } from '@/components/ui/select' -import { usePlanAsignaturas, usePlanLineas, useUpdateAsignatura } from '@/data' +import { + useCreateLinea, + useDeleteLinea, + usePlanAsignaturas, + usePlanLineas, + useUpdateAsignatura, + useUpdateLinea, +} from '@/data' // --- Mapeadores (Fuera del componente para mayor limpieza) --- const mapLineasToLineaCurricular = ( @@ -166,8 +174,11 @@ export const Route = createFileRoute('/planes/$planId/_detalle/mapa')({ function MapaCurricularPage() { const { planId } = Route.useParams() // Idealmente usa el ID de la ruta const { ciclo } = Route.useSearch() - console.log(ciclo) - + 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) @@ -189,55 +200,78 @@ function MapaCurricularPage() { const manejarAgregarLinea = (nombre: string) => { const nombreNormalizado = nombre.trim() - // 1. Validar que no esté vacío + // 1. Validar vacío if (!nombreNormalizado) return - // 2. Validar duplicados (Insensible a mayúsculas/minúsculas y acentos) - const nombreParaComparar = nombreNormalizado + // 2. Validar duplicados en el estado local (Insensible a mayúsculas/acentos) + const nombreBusqueda = nombreNormalizado .toLowerCase() .normalize('NFD') .replace(/[\u0300-\u036f]/g, '') const yaExiste = lineas.some((l) => { - const lineaNombreBase = l.nombre + const lineaExistente = l.nombre .toLowerCase() .normalize('NFD') .replace(/[\u0300-\u036f]/g, '') - return lineaNombreBase === nombreParaComparar + return lineaExistente === nombreBusqueda }) if (yaExiste) { - alert(`La línea "${nombreNormalizado}" ya existe.`) - return + alert(`La línea "${nombreNormalizado}" ya existe en este plan.`) + return // DETIENE la ejecución aquí, no llega a la mutación } - // 3. Validar Área Común (usando tu lógica previa) - const esAreaComun = - nombreNormalizado.toLowerCase() === 'área común' || - nombreNormalizado.toLowerCase() === 'area comun' + // 3. Si pasa las validaciones, procedemos con la persistencia + const maxOrden = lineas.reduce((max, l) => Math.max(max, l.orden || 0), 0) - if (esAreaComun && hasAreaComun) { - alert('El Área Común ya ha sido agregada.') - return - } - - // 4. Agregar la línea si todo está bien - const nueva = { - id: crypto.randomUUID(), - nombre: nombreNormalizado, - orden: lineas.length + 1, - color: '#1976d2', - } - - setLineas([...lineas, nueva]) - - if (esAreaComun) { - setHasAreaComun(true) - } - - setNombreNuevaLinea('') // Limpiar input + createLinea( + { + nombre: nombreNormalizado, + plan_estudio_id: planId, + orden: maxOrden + 1, + area: 'sin asignar', + }, + { + onSuccess: (nueva) => { + // Sincronización local que ya teníamos + const mapeada = { + id: nueva.id, + nombre: nueva.nombre, + orden: nueva.orden, + color: '#1976d2', + } + setLineas((prev) => [...prev, mapeada]) + setNombreNuevaLinea('') + }, + }, + ) } + const guardarEdicionLinea = (id: string) => { + if (!tempNombreLinea.trim()) { + setEditingLineaId(null) + return + } + updateLineaApi( + { + lineaId: id, + patch: { nombre: tempNombreLinea.trim() }, + }, + { + onSuccess: (lineaActualizada) => { + // ACTUALIZACIÓN MANUAL DEL ESTADO LOCAL + setLineas((prev) => + prev.map((l) => + l.id === id ? { ...l, nombre: lineaActualizada.nombre } : l, + ), + ) + setEditingLineaId(null) + setTempNombreLinea('') + }, + }, + ) + } const tieneAreaComun = useMemo(() => { return lineas.some( (l) => @@ -313,14 +347,37 @@ function MapaCurricularPage() { } const borrarLinea = (id: string) => { - setAsignaturas((prev) => - prev.map((m) => - m.lineaCurricularId === id - ? { ...m, ciclo: null, lineaCurricularId: null } - : m, - ), - ) - setLineas((prev) => prev.filter((l) => l.id !== id)) + // 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.', + ) + ) { + 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) => + asig.lineaCurricularId === id + ? { ...asig, ciclo: null, lineaCurricularId: null } + : asig, + ), + ) + + // Segundo: Quitamos la línea del estado + setLineas((prev) => prev.filter((l) => l.id !== id)) + }, + onError: (error) => { + console.error(error) + alert('No se pudo eliminar la línea. Verifica si tiene dependencias.') + }, + }) } // --- Selectores/Cálculos --- @@ -523,11 +580,33 @@ function MapaCurricularPage() {
- {linea.nombre} + {editingLineaId === linea.id ? ( + setTempNombreLinea(e.target.value)} + onBlur={() => guardarEdicionLinea(linea.id)} + onKeyDown={(e) => + e.key === 'Enter' && guardarEdicionLinea(linea.id) + } + /> + ) : ( + { + setEditingLineaId(linea.id) + setTempNombreLinea(linea.nombre) + }} + > + {linea.nombre} + + )} + borrarLinea(linea.id)} + onClick={() => borrarLinea(linea.id)} // Aquí también podrías añadir una mutación delete />
-- 2.49.1