From 261dec7fa91785899780f36da38f2ce50e21fac8 Mon Sep 17 00:00:00 2001 From: "Roberto.silva" Date: Tue, 3 Feb 2026 16:05:05 -0600 Subject: [PATCH] Se agrega persistencia en tab de datos y mapa curricular fix #42 fix #54 --- src/data/api/plans.api.ts | 2 +- src/data/api/subjects.api.ts | 37 ++++-- src/data/hooks/useSubjects.ts | 44 +++++-- src/routes/planes/$planId/_detalle/mapa.tsx | 125 +++++++++++++++++--- 4 files changed, 171 insertions(+), 37 deletions(-) diff --git a/src/data/api/plans.api.ts b/src/data/api/plans.api.ts index ff40312..0ef5658 100644 --- a/src/data/api/plans.api.ts +++ b/src/data/api/plans.api.ts @@ -165,7 +165,7 @@ export async function plan_asignaturas_list( const { data, error } = await supabase .from('asignaturas') .select( - 'id,plan_estudio_id,estructura_id,facultad_propietaria_id,codigo,nombre,tipo,creditos,horas_semana,numero_ciclo,linea_plan_id,orden_celda,datos,contenido_tematico,tipo_origen,meta_origen,creado_por,actualizado_por,creado_en,actualizado_en', + 'id,plan_estudio_id,horas_academicas, horas_independientes,estructura_id,codigo,nombre,tipo,creditos,numero_ciclo,linea_plan_id,orden_celda,datos,contenido_tematico,tipo_origen,meta_origen,creado_por,actualizado_por,creado_en,actualizado_en', ) .eq('plan_estudio_id', planId) .order('numero_ciclo', { ascending: true, nullsFirst: false }) diff --git a/src/data/api/subjects.api.ts b/src/data/api/subjects.api.ts index cfd6a67..a2d28ef 100644 --- a/src/data/api/subjects.api.ts +++ b/src/data/api/subjects.api.ts @@ -1,6 +1,9 @@ import { supabaseBrowser } from '../supabase/client' import { invokeEdge } from '../supabase/invokeEdge' + import { throwIfError, requireData } from './_helpers' + +import type { DocumentoResult } from './plans.api' import type { Asignatura, BibliografiaAsignatura, @@ -8,7 +11,6 @@ import type { TipoAsignatura, UUID, } from '../types/domain' -import type { DocumentoResult } from './plans.api' const EDGE = { subjects_create_manual: 'subjects_create_manual', @@ -32,7 +34,7 @@ export async function subjects_get(subjectId: UUID): Promise { .from('asignaturas') .select( ` - id,plan_estudio_id,estructura_id,facultad_propietaria_id,codigo,nombre,tipo,creditos,horas_semana,numero_ciclo,linea_plan_id,orden_celda,datos,contenido_tematico,tipo_origen,meta_origen,creado_por,actualizado_por,creado_en,actualizado_en, + id,plan_estudio_id,estructura_id,codigo,nombre,tipo,creditos,numero_ciclo,linea_plan_id,orden_celda,datos,contenido_tematico,tipo_origen,meta_origen,creado_por,actualizado_por,creado_en,actualizado_en, planes_estudio( id,carrera_id,estructura_id,nombre,nivel,tipo_ciclo,numero_ciclos,datos,estado_actual_id,activo,tipo_origen,meta_origen,creado_por,actualizado_por,creado_en,actualizado_en, carreras(id,facultad_id,nombre,nombre_corto,clave_sep,activa, facultades(id,nombre,nombre_corto,color,icono)) @@ -49,7 +51,7 @@ export async function subjects_get(subjectId: UUID): Promise { export async function subjects_history( subjectId: UUID, -): Promise { +): Promise> { const supabase = supabaseBrowser() const { data, error } = await supabase .from('cambios_asignatura') @@ -65,7 +67,7 @@ export async function subjects_history( export async function subjects_bibliografia_list( subjectId: UUID, -): Promise { +): Promise> { const supabase = supabaseBrowser() const { data, error } = await supabase .from('bibliografia_asignatura') @@ -112,9 +114,9 @@ export async function ai_generate_subject(payload: { iaConfig: { descripcionEnfoque: string notasAdicionales?: string - archivosExistentesIds?: UUID[] - repositoriosIds?: UUID[] - archivosAdhocIds?: UUID[] + archivosExistentesIds?: Array + repositoriosIds?: Array + archivosAdhocIds?: Array usarMCP?: boolean } }): Promise { @@ -145,7 +147,7 @@ export async function subjects_clone_from_existing(payload: { export async function subjects_import_from_file(payload: { planId: UUID archivoWordAsignaturaId: UUID - archivosAdicionalesIds?: UUID[] + archivosAdicionalesIds?: Array }): Promise { return invokeEdge(EDGE.subjects_import_from_file, payload) } @@ -175,7 +177,7 @@ export async function subjects_update_fields( export async function subjects_update_contenido( subjectId: UUID, - unidades: any[], + unidades: Array, ): Promise { return invokeEdge(EDGE.subjects_update_contenido, { subjectId, @@ -224,3 +226,20 @@ export async function subjects_get_document( subjectId, }) } + +export async function asignaturas_update( + asignaturaId: UUID, + patch: Partial, // O tu tipo específico para el Patch de materias +): Promise { + const supabase = supabaseBrowser() + + const { data, error } = await supabase + .from('asignaturas') + .update(patch) + .eq('id', asignaturaId) + .select() // Trae la materia actualizada + .single() + + throwIfError(error) + return requireData(data, 'No se pudo actualizar la asignatura.') +} diff --git a/src/data/hooks/useSubjects.ts b/src/data/hooks/useSubjects.ts index afeedf0..ff4d38c 100644 --- a/src/data/hooks/useSubjects.ts +++ b/src/data/hooks/useSubjects.ts @@ -1,13 +1,8 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' -import { qk } from '../query/keys' -import type { UUID } from '../types/domain' -import type { - BibliografiaUpsertInput, - SubjectsCreateManualInput, - SubjectsUpdateFieldsPatch, -} from '../api/subjects.api' + import { ai_generate_subject, + asignaturas_update, subjects_bibliografia_list, subjects_clone_from_existing, subjects_create_manual, @@ -21,6 +16,14 @@ import { subjects_update_contenido, subjects_update_fields, } from '../api/subjects.api' +import { qk } from '../query/keys' + +import type { + BibliografiaUpsertInput, + SubjectsCreateManualInput, + SubjectsUpdateFieldsPatch, +} from '../api/subjects.api' +import type { UUID } from '../types/domain' export function useSubject(subjectId: UUID | null | undefined) { return useQuery({ @@ -159,7 +162,7 @@ export function useUpdateSubjectContenido() { const qc = useQueryClient() return useMutation({ - mutationFn: (vars: { subjectId: UUID; unidades: any[] }) => + mutationFn: (vars: { subjectId: UUID; unidades: Array }) => subjects_update_contenido(vars.subjectId, vars.unidades), onSuccess: (updated) => { qc.setQueryData(qk.asignatura(updated.id), updated) @@ -194,3 +197,28 @@ export function useGenerateSubjectDocumento() { }, }) } + +export function useUpdateAsignatura() { + const qc = useQueryClient() + + return useMutation({ + mutationFn: (vars: { + asignaturaId: UUID + patch: Partial + }) => asignaturas_update(vars.asignaturaId, vars.patch), + + onSuccess: (updated) => { + // 1. Actualizamos la materia específica en la caché si tienes un query de "detalle" + qc.setQueryData(['asignatura', updated.id], updated) + + // 2. IMPORTANTÍSIMO: Invalidamos la lista de materias del plan + // para que el mapa curricular vea los cambios (créditos, horas, nombre, etc.) + qc.invalidateQueries({ + queryKey: ['plan_asignaturas', updated.plan_estudio_id], + }) + + // 3. Si tienes una lista general de asignaturas, también la invalidamos + qc.invalidateQueries({ queryKey: ['asignaturas', 'list'] }) + }, + }) +} diff --git a/src/routes/planes/$planId/_detalle/mapa.tsx b/src/routes/planes/$planId/_detalle/mapa.tsx index edfa4e5..3241506 100644 --- a/src/routes/planes/$planId/_detalle/mapa.tsx +++ b/src/routes/planes/$planId/_detalle/mapa.tsx @@ -33,7 +33,7 @@ import { SelectTrigger, SelectValue, } from '@/components/ui/select' -import { usePlanAsignaturas, usePlanLineas } from '@/data' +import { usePlanAsignaturas, usePlanLineas, useUpdateAsignatura } from '@/data' // --- Mapeadores (Fuera del componente para mayor limpieza) --- const mapLineasToLineaCurricular = ( @@ -60,8 +60,9 @@ const mapAsignaturasToAsignaturas = ( tipo: asig.tipo === 'OBLIGATORIA' ? 'obligatoria' : 'optativa', estado: 'borrador', orden: asig.orden_celda ?? 0, - hd: Math.floor((asig.horas_semana ?? 0) / 2), - hi: Math.ceil((asig.horas_semana ?? 0) / 2), + // Mapeo directo de los nuevos campos de la API + hd: asig.horas_academicas ?? 0, + hi: asig.horas_independientes ?? 0, prerrequisitos: [], })) } @@ -183,6 +184,7 @@ function MapaCurricularPage() { useState(null) const [hasAreaComun, setHasAreaComun] = useState(false) const [nombreNuevaLinea, setNombreNuevaLinea] = useState('') // Para el input de nombre personalizado + const { mutate: updateAsignatura, isPending } = useUpdateAsignatura() const manejarAgregarLinea = (nombre: string) => { const nombreNormalizado = nombre.trim() @@ -268,13 +270,41 @@ function MapaCurricularPage() { setAsignaturas((prev) => prev.map((m) => (m.id === editingData.id ? { ...editingData } : m)), ) - setIsEditModalOpen(false) + // setIsEditModalOpen(false) + // Preparamos el patch con la estructura de tu tabla + const patch = { + 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, + tipo: editingData.tipo.toUpperCase(), // Asegurar que coincida con el ENUM (OBLIGATORIA/OPTATIVA) + // datos: editingData.datos, // Si editaste algo del JSONB + } + + updateAsignatura( + { asignaturaId: editingData.id, patch }, + { + 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.') + }, + }, + ) } // 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) + const unassignedAsignaturas = asignaturas.filter( + (m) => m.ciclo === null || m.lineaCurricularId === null, + ) // --- Lógica de Gestión --- const agregarLinea = (nombre: string) => { @@ -309,7 +339,7 @@ function MapaCurricularPage() { const getSubtotalLinea = (lineaId: string) => { return asignaturas - .filter((m) => m.lineaCurricularId === lineaId && m.ciclo !== null) + .filter((m) => m.lineaCurricularId === lineaId && m.ciclo !== null) // Aseguramos que pertenezca a la línea Y tenga ciclo .reduce( (acc, m) => ({ cr: acc.cr + (m.creditos || 0), @@ -332,6 +362,7 @@ function MapaCurricularPage() { ) => { e.preventDefault() if (draggedAsignatura) { + // 1. Actualización optimista del UI setAsignaturas((prev) => prev.map((m) => m.id === draggedAsignatura @@ -339,6 +370,23 @@ function MapaCurricularPage() { : m, ), ) + + // 2. Persistir en la API + const patch = { + numero_ciclo: ciclo, + linea_plan_id: lineaId, + } + + updateAsignatura( + { asignaturaId: draggedAsignatura, patch }, + { + onError: (error) => { + console.error('Error al mover:', error) + // Opcional: Revertir el estado local si falla + }, + }, + ) + setDraggedAsignatura(null) } } @@ -373,10 +421,15 @@ function MapaCurricularPage() {

- {asignaturas.filter((m) => !m.ciclo).length > 0 && ( + {asignaturas.filter((m) => !m.ciclo || !m.lineaCurricularId).length > + 0 && ( {' '} - {asignaturas.filter((m) => !m.ciclo).length} sin asignar + { + asignaturas.filter((m) => !m.ciclo || !m.lineaCurricularId) + .length + }{' '} + sin asignar )} @@ -482,6 +535,8 @@ function MapaCurricularPage() {
handleDrop(e, null, null)} + // AHORA: Usamos las variables 'ciclo' y 'linea.id' del map onDrop={(e) => handleDrop(e, ciclo, linea.id)} className="min-h-[140px] space-y-2 rounded-xl border-2 border-dashed border-slate-100 bg-slate-50/20 p-2" > @@ -593,7 +648,10 @@ function MapaCurricularPage() { {/* Modal de Edición */} - + e.preventDefault()} + > Editar Asignatura @@ -739,32 +797,61 @@ function MapaCurricularPage() {
- {/* Fila 4: Seriación (Igual a tu imagen) */} + {/* Fila 4: Seriación (Prerrequisitos) */}
- { + // Evitamos duplicados en la lista de prerrequisitos local + if (!editingData.prerrequisitos.includes(val)) { + setEditingData({ + ...editingData, + prerrequisitos: [...editingData.prerrequisitos, val], + }) + } + }} + > - {asignaturas.map((m) => ( - - {m.nombre} - - ))} + {/* FILTRO CLAVE: + Solo mostramos materias cuyo ID sea diferente al de la materia que estamos editando + */} + {asignaturas + .filter((m) => m.id !== editingData.id) + .map((m) => ( + + {m.nombre} ({m.clave}) + + ))} -
- {/* Aquí usamos el array vacío que inicializamos en el mapeador */} + + {/* Visualización de los prerrequisitos seleccionados */} +
{editingData.prerrequisitos.map((pre) => ( - {pre} × + {pre} + ))}