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 */}
- {/* Fila 4: Seriación (Igual a tu imagen) */}
+ {/* Fila 4: Seriación (Prerrequisitos) */}
-