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, CarreraRow, CambioAsignatura, EstructuraAsignatura, FacultadRow, PlanEstudioRow, TipoAsignatura, UUID, } from '../types/domain' import type { AsignaturaSugerida, DataAsignaturaSugerida, } from '@/features/asignaturas/nueva/types' import type { Database, TablesInsert } from '@/types/supabase' const EDGE = { generate_subject_suggestions: 'generate-subject-suggestions', subjects_create_manual: 'subjects_create_manual', ai_generate_subject: 'ai-generate-subject', subjects_persist_from_ai: 'subjects_persist_from_ai', subjects_clone_from_existing: 'subjects_clone_from_existing', subjects_import_from_file: 'subjects_import_from_file', subjects_update_fields: 'subjects_update_fields', subjects_update_bibliografia: 'subjects_update_bibliografia', subjects_generate_document: 'subjects_generate_document', subjects_get_document: 'subjects_get_document', } as const export type ContenidoTemaApi = | string | { nombre: string horasEstimadas?: number descripcion?: string [key: string]: unknown } /** * Estructura persistida en `asignaturas.contenido_tematico`. * La BDD guarda un arreglo de unidades, cada una con temas (strings u objetos). */ export type ContenidoApi = { unidad: number titulo: string temas: Array [key: string]: unknown } export type FacultadInSubject = Pick< FacultadRow, 'id' | 'nombre' | 'nombre_corto' | 'color' | 'icono' > export type CarreraInSubject = Pick< CarreraRow, 'id' | 'facultad_id' | 'nombre' | 'nombre_corto' | 'clave_sep' | 'activa' > & { facultades: FacultadInSubject | null } export type PlanEstudioInSubject = Pick< PlanEstudioRow, | '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: CarreraInSubject | null } export type EstructuraAsignaturaInSubject = Pick< EstructuraAsignatura, 'id' | 'nombre' | 'version' | 'definicion' > /** * Tipo real que devuelve `subjects_get` (asignatura + relaciones seleccionadas). * Nota: `asignaturas_update` (update directo) NO devuelve estas relaciones. */ export type AsignaturaDetail = Omit & { contenido_tematico: Array | null planes_estudio: PlanEstudioInSubject | null estructuras_asignatura: EstructuraAsignaturaInSubject | null } export async function subjects_get(subjectId: UUID): Promise { const supabase = supabaseBrowser() const { data, error } = await supabase .from('asignaturas') .select( ` id,plan_estudio_id,estructura_id,codigo,nombre,tipo,creditos,numero_ciclo,linea_plan_id,orden_celda,estado,datos,contenido_tematico,horas_academicas,horas_independientes,asignatura_hash,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)) ), estructuras_asignatura(id,nombre,version,definicion) `, ) .eq('id', subjectId) .single() throwIfError(error) return requireData( data, 'Asignatura no encontrada.', ) as unknown as AsignaturaDetail } export async function subjects_history( subjectId: UUID, ): Promise> { const supabase = supabaseBrowser() const { data, error } = await supabase .from('cambios_asignatura') .select( 'id,asignatura_id,cambiado_por,cambiado_en,tipo,campo,valor_anterior,valor_nuevo,fuente,interaccion_ia_id', ) .eq('asignatura_id', subjectId) .order('cambiado_en', { ascending: false }) throwIfError(error) return data ?? [] } export async function subjects_bibliografia_list( subjectId: UUID, ): Promise> { const supabase = supabaseBrowser() const { data, error } = await supabase .from('bibliografia_asignatura') .select( 'id,asignatura_id,tipo,cita,tipo_fuente,biblioteca_item_id,creado_por,creado_en,actualizado_en', ) .eq('asignatura_id', subjectId) .order('tipo', { ascending: true }) .order('creado_en', { ascending: true }) throwIfError(error) return data ?? [] } export async function subjects_create_manual( payload: TablesInsert<'asignaturas'>, ): Promise { const supabase = supabaseBrowser() const { data, error } = await supabase .from('asignaturas') .insert(payload) .select() .single() throwIfError(error) return requireData(data, 'No se pudo crear la asignatura.') } /** * Nuevo payload unificado (JSON) para la Edge `ai_generate_subject`. * - Siempre incluye `datosUpdate.plan_estudio_id`. * - `datosUpdate.id` es opcional (si no existe, la Edge puede crear). * En el frontend, insertamos primero y usamos `id` para actualizar. */ export type AISubjectUnifiedInput = { datosUpdate: Partial<{ id: string plan_estudio_id: string estructura_id: string nombre: string codigo: string | null tipo: string | null creditos: number horas_academicas: number | null horas_independientes: number | null numero_ciclo: number | null linea_plan_id: string | null orden_celda: number | null }> & { plan_estudio_id: string } iaConfig?: { descripcionEnfoqueAcademico?: string instruccionesAdicionalesIA?: string archivosAdjuntos?: Array } } export async function subjects_get_maybe( subjectId: UUID, ): Promise { const supabase = supabaseBrowser() const { data, error } = await supabase .from('asignaturas') .select('id,plan_estudio_id,estado') .eq('id', subjectId) .maybeSingle() throwIfError(error) return (data ?? null) as unknown as Asignatura | null } export type GenerateSubjectSuggestionsInput = { plan_estudio_id: UUID enfoque?: string cantidad_de_sugerencias: number sugerencias_conservadas: Array<{ nombre: string; descripcion: string }> } export async function generate_subject_suggestions( input: GenerateSubjectSuggestionsInput, ): Promise> { const raw = await invokeEdge>( EDGE.generate_subject_suggestions, input, { headers: { 'Content-Type': 'application/json' } }, ) return raw.map( (s): AsignaturaSugerida => ({ id: crypto.randomUUID(), selected: false, source: 'IA', nombre: s.nombre, codigo: s.codigo, tipo: s.tipo ?? null, creditos: s.creditos ?? null, horasAcademicas: s.horasAcademicas ?? null, horasIndependientes: s.horasIndependientes ?? null, descripcion: s.descripcion, linea_plan_id: null, numero_ciclo: null, }), ) } export async function ai_generate_subject( input: AISubjectUnifiedInput, ): Promise { return invokeEdge(EDGE.ai_generate_subject, input, { headers: { 'Content-Type': 'application/json' }, }) } export async function subjects_persist_from_ai(payload: { planId: UUID jsonAsignatura: any }): Promise { return invokeEdge(EDGE.subjects_persist_from_ai, payload) } export async function subjects_clone_from_existing(payload: { asignaturaOrigenId: UUID planDestinoId: UUID overrides?: Partial<{ nombre: string codigo: string tipo: TipoAsignatura creditos: number horas_semana: number }> }): Promise { return invokeEdge(EDGE.subjects_clone_from_existing, payload) } export async function subjects_import_from_file(payload: { planId: UUID archivoWordAsignaturaId: UUID archivosAdicionalesIds?: Array }): Promise { return invokeEdge(EDGE.subjects_import_from_file, payload) } /** Guardado de tarjetas/fields (Edge: merge server-side en asignaturas.datos y columnas) */ export type SubjectsUpdateFieldsPatch = Partial<{ codigo: string | null nombre: string tipo: TipoAsignatura creditos: number horas_semana: number | null numero_ciclo: number | null linea_plan_id: UUID | null datos: Record }> export async function subjects_update_fields( subjectId: UUID, patch: SubjectsUpdateFieldsPatch, ): Promise { return invokeEdge(EDGE.subjects_update_fields, { subjectId, patch, }) } export async function subjects_update_contenido( subjectId: UUID, unidades: Array, ): Promise { const supabase = supabaseBrowser() type AsignaturaUpdate = Database['public']['Tables']['asignaturas']['Update'] const { data, error } = await supabase .from('asignaturas') .update({ contenido_tematico: unidades as unknown as AsignaturaUpdate['contenido_tematico'], }) .eq('id', subjectId) .select() .single() throwIfError(error) return requireData(data, 'No se pudo actualizar la asignatura.') } export type BibliografiaUpsertInput = Array<{ id?: UUID tipo: 'BASICA' | 'COMPLEMENTARIA' cita: string tipo_fuente?: 'MANUAL' | 'BIBLIOTECA' biblioteca_item_id?: string | null }> export async function subjects_update_bibliografia( subjectId: UUID, entries: BibliografiaUpsertInput, ): Promise<{ ok: true }> { return invokeEdge<{ ok: true }>(EDGE.subjects_update_bibliografia, { subjectId, entries, }) } /** Documento SEP asignatura */ /* export type DocumentoResult = { archivoId: UUID; signedUrl: string; mimeType?: string; nombre?: string; }; */ export async function subjects_generate_document( subjectId: UUID, ): Promise { return invokeEdge(EDGE.subjects_generate_document, { subjectId, }) } export async function subjects_get_document( subjectId: UUID, ): Promise { return invokeEdge(EDGE.subjects_get_document, { subjectId, }) } export async function subjects_get_structure_catalog(): Promise< Array > { const supabase = supabaseBrowser() const { data, error } = await supabase .from('estructuras_asignatura') .select('*') .order('nombre', { ascending: true }) if (error) { throw error } return data } 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.') } // 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 } export async function bibliografia_insert(entry: { asignatura_id: string tipo: 'BASICA' | 'COMPLEMENTARIA' cita: string tipo_fuente: 'MANUAL' | 'BIBLIOTECA' biblioteca_item_id?: string | null }) { const supabase = supabaseBrowser() const { data, error } = await supabase .from('bibliografia_asignatura') .insert([entry]) .select() .single() if (error) throw error return data } export async function bibliografia_update( id: string, updates: { cita?: string tipo?: 'BASICA' | 'COMPLEMENTARIA' }, ) { const supabase = supabaseBrowser() const { data, error } = await supabase .from('bibliografia_asignatura') .update(updates) // Ahora 'updates' es compatible con lo que espera Supabase .eq('id', id) .select() .single() if (error) throw error return data } export async function bibliografia_delete(id: string) { const supabase = supabaseBrowser() const { error } = await supabase .from('bibliografia_asignatura') .delete() .eq('id', id) if (error) throw error return id }