refactor: rename Materia to Asignatura across the codebase

- Updated type definitions and interfaces to replace 'Materia' with 'Asignatura'.
- Refactored components and routes to reflect the new naming convention.
- Adjusted related types and constants for consistency.
- Removed the old Materia type definition and added Asignatura type definition.
- Ensured all references in UI components and logic are updated accordingly.

fix #50
This commit is contained in:
2026-01-30 08:13:30 -06:00
parent 2c702d7d67
commit d0b05256b0
20 changed files with 693 additions and 693 deletions

View File

@@ -1,45 +1,56 @@
import type { PostgrestError, AuthError, SupabaseClient } from "@supabase/supabase-js";
import type { Database } from "../types/database";
import type { Database } from '../types/database'
import type {
PostgrestError,
AuthError,
SupabaseClient,
} from '@supabase/supabase-js'
export class ApiError extends Error {
constructor(
message: string,
public readonly code?: string,
public readonly details?: unknown,
public readonly hint?: string
public readonly hint?: string,
) {
super(message);
this.name = "ApiError";
super(message)
this.name = 'ApiError'
}
}
export function throwIfError(error: PostgrestError | AuthError | null): void {
if (!error) return;
const anyErr = error as any;
if (!error) return
const anyErr = error as any
throw new ApiError(
anyErr.message ?? "Error inesperado",
anyErr.message ?? 'Error inesperado',
anyErr.code,
anyErr.details,
anyErr.hint
);
anyErr.hint,
)
}
export function requireData<T>(data: T | null | undefined, message = "Respuesta vacía"): T {
if (data === null || data === undefined) throw new ApiError(message);
return data;
export function requireData<T>(
data: T | null | undefined,
message = 'Respuesta vacía',
): T {
if (data === null || data === undefined) throw new ApiError(message)
return data
}
export async function getUserIdOrThrow(supabase: SupabaseClient<Database>): Promise<string> {
const { data, error } = await supabase.auth.getUser();
throwIfError(error);
if (!data?.user?.id) throw new ApiError("No hay sesión activa (auth).");
return data.user.id;
export async function getUserIdOrThrow(
supabase: SupabaseClient<Database>,
): Promise<string> {
const { data, error } = await supabase.auth.getUser()
throwIfError(error)
if (!data?.user?.id) throw new ApiError('No hay sesión activa (auth).')
return data.user.id
}
export function buildRange(limit?: number, offset?: number): { from?: number; to?: number } {
if (!limit) return {};
const from = Math.max(0, offset ?? 0);
const to = from + Math.max(1, limit) - 1;
return { from, to };
export function buildRange(
limit?: number,
offset?: number,
): { from?: number; to?: number } {
if (!limit) return {}
const from = Math.max(0, offset ?? 0)
const to = from + Math.max(1, limit) - 1
return { from, to }
}

View File

@@ -1,32 +1,32 @@
import { invokeEdge } from "../supabase/invokeEdge";
import type { UUID } from "../types/domain";
import { invokeEdge } from '../supabase/invokeEdge'
import type { UUID } from '../types/domain'
/**
* Metadata “canónica” para UI (archivo OpenAI + espejo en Supabase)
* Se apoya en tu tabla `archivos`.
*/
export type AppFile = {
id: UUID; // id interno (tabla archivos)
openai_file_id: string; // id OpenAI
nombre: string;
mime_type: string | null;
bytes: number | null;
id: UUID // id interno (tabla archivos)
openai_file_id: string // id OpenAI
nombre: string
mime_type: string | null
bytes: number | null
// espejo Supabase para preview/descarga
ruta_storage: string | null; // "bucket/path"
signed_url?: string | null;
ruta_storage: string | null // "bucket/path"
signed_url?: string | null
// auditoría/evidencia
temporal: boolean;
notas?: string | null;
temporal: boolean
notas?: string | null
subido_en: string;
};
subido_en: string
}
const EDGE = {
upload: "openai_files_upload",
remove: "openai_files_delete",
} as const;
upload: 'openai_files_upload',
remove: 'openai_files_delete',
} as const
/**
* Sube archivo a OpenAI y (opcional) crea espejo en Storage
@@ -37,28 +37,28 @@ export async function openai_files_upload(payload: {
* Si tu Edge soporta multipart: manda File/Blob directo.
* Si no, manda base64/bytes (según tu implementación).
*/
file: File;
file: File
/** “temporal” = evidencia usada para generar plan/materia */
temporal?: boolean;
/** “temporal” = evidencia usada para generar plan/asignatura */
temporal?: boolean
/** contexto para auditoría */
contexto?: {
planId?: UUID;
asignaturaId?: UUID;
motivo?: "WIZARD_PLAN" | "WIZARD_MATERIA" | "ADHOC";
};
planId?: UUID
asignaturaId?: UUID
motivo?: 'WIZARD_PLAN' | 'WIZARD_MATERIA' | 'ADHOC'
}
/** si quieres forzar espejo para preview siempre */
mirrorToSupabase?: boolean;
mirrorToSupabase?: boolean
}): Promise<AppFile> {
return invokeEdge<AppFile>(EDGE.upload, payload);
return invokeEdge<AppFile>(EDGE.upload, payload)
}
export async function openai_files_delete(payload: {
openaiFileId: string;
openaiFileId: string
/** si quieres borrar también espejo y registro */
hardDelete?: boolean;
hardDelete?: boolean
}): Promise<{ ok: true }> {
return invokeEdge<{ ok: true }>(EDGE.remove, payload);
return invokeEdge<{ ok: true }>(EDGE.remove, payload)
}

View File

@@ -333,7 +333,7 @@ export async function plans_import_from_files(payload: {
}
archivoWordPlanId: UUID
archivoMapaExcelId?: UUID | null
archivoMateriasExcelId?: UUID | null
archivoAsignaturasExcelId?: UUID | null
}): Promise<PlanEstudio> {
return invokeEdge<PlanEstudio>(EDGE.plans_import_from_files, payload)
}

View File

@@ -1,35 +1,35 @@
import { supabaseBrowser } from "../supabase/client";
import { invokeEdge } from "../supabase/invokeEdge";
import { throwIfError, requireData } from "./_helpers";
import { supabaseBrowser } from '../supabase/client'
import { invokeEdge } from '../supabase/invokeEdge'
import { throwIfError, requireData } from './_helpers'
import type {
Asignatura,
BibliografiaAsignatura,
CambioAsignatura,
TipoAsignatura,
UUID,
} from "../types/domain";
import type { DocumentoResult } from "./plans.api";
} from '../types/domain'
import type { DocumentoResult } from './plans.api'
const EDGE = {
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_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_contenido: "subjects_update_contenido",
subjects_update_bibliografia: "subjects_update_bibliografia",
subjects_update_fields: 'subjects_update_fields',
subjects_update_contenido: 'subjects_update_contenido',
subjects_update_bibliografia: 'subjects_update_bibliografia',
subjects_generate_document: "subjects_generate_document",
subjects_get_document: "subjects_get_document",
} as const;
subjects_generate_document: 'subjects_generate_document',
subjects_get_document: 'subjects_get_document',
} as const
export async function subjects_get(subjectId: UUID): Promise<Asignatura> {
const supabase = supabaseBrowser();
const supabase = supabaseBrowser()
const { data, error } = await supabase
.from("asignaturas")
.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,
@@ -38,144 +38,170 @@ export async function subjects_get(subjectId: UUID): Promise<Asignatura> {
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();
.eq('id', subjectId)
.single()
throwIfError(error);
return requireData(data, "Materia no encontrada.");
throwIfError(error)
return requireData(data, 'Asignatura no encontrada.')
}
export async function subjects_history(subjectId: UUID): Promise<CambioAsignatura[]> {
const supabase = supabaseBrowser();
export async function subjects_history(
subjectId: UUID,
): Promise<CambioAsignatura[]> {
const supabase = supabaseBrowser()
const { data, error } = await supabase
.from("cambios_asignatura")
.from('cambios_asignatura')
.select(
"id,asignatura_id,cambiado_por,cambiado_en,tipo,campo,valor_anterior,valor_nuevo,fuente,interaccion_ia_id"
'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 });
.eq('asignatura_id', subjectId)
.order('cambiado_en', { ascending: false })
throwIfError(error);
return data ?? [];
throwIfError(error)
return data ?? []
}
export async function subjects_bibliografia_list(subjectId: UUID): Promise<BibliografiaAsignatura[]> {
const supabase = supabaseBrowser();
export async function subjects_bibliografia_list(
subjectId: UUID,
): Promise<BibliografiaAsignatura[]> {
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 });
.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 ?? [];
throwIfError(error)
return data ?? []
}
/** Wizard: crear materia manual (Edge Function) */
/** Wizard: crear asignatura manual (Edge Function) */
export type SubjectsCreateManualInput = {
planId: UUID;
planId: UUID
datosBasicos: {
nombre: string;
clave?: string;
tipo: TipoAsignatura;
creditos: number;
horasSemana?: number;
estructuraId: UUID;
};
};
nombre: string
clave?: string
tipo: TipoAsignatura
creditos: number
horasSemana?: number
estructuraId: UUID
}
}
export async function subjects_create_manual(payload: SubjectsCreateManualInput): Promise<Asignatura> {
return invokeEdge<Asignatura>(EDGE.subjects_create_manual, payload);
export async function subjects_create_manual(
payload: SubjectsCreateManualInput,
): Promise<Asignatura> {
return invokeEdge<Asignatura>(EDGE.subjects_create_manual, payload)
}
export async function ai_generate_subject(payload: {
planId: UUID;
planId: UUID
datosBasicos: {
nombre: string;
clave?: string;
tipo: TipoAsignatura;
creditos: number;
horasSemana?: number;
estructuraId: UUID;
};
nombre: string
clave?: string
tipo: TipoAsignatura
creditos: number
horasSemana?: number
estructuraId: UUID
}
iaConfig: {
descripcionEnfoque: string;
notasAdicionales?: string;
archivosExistentesIds?: UUID[];
repositoriosIds?: UUID[];
archivosAdhocIds?: UUID[];
usarMCP?: boolean;
};
descripcionEnfoque: string
notasAdicionales?: string
archivosExistentesIds?: UUID[]
repositoriosIds?: UUID[]
archivosAdhocIds?: UUID[]
usarMCP?: boolean
}
}): Promise<any> {
return invokeEdge<any>(EDGE.ai_generate_subject, payload);
return invokeEdge<any>(EDGE.ai_generate_subject, payload)
}
export async function subjects_persist_from_ai(payload: { planId: UUID; jsonMateria: any }): Promise<Asignatura> {
return invokeEdge<Asignatura>(EDGE.subjects_persist_from_ai, payload);
export async function subjects_persist_from_ai(payload: {
planId: UUID
jsonAsignatura: any
}): Promise<Asignatura> {
return invokeEdge<Asignatura>(EDGE.subjects_persist_from_ai, payload)
}
export async function subjects_clone_from_existing(payload: {
materiaOrigenId: UUID;
planDestinoId: UUID;
asignaturaOrigenId: UUID
planDestinoId: UUID
overrides?: Partial<{
nombre: string;
codigo: string;
tipo: TipoAsignatura;
creditos: number;
horas_semana: number;
}>;
nombre: string
codigo: string
tipo: TipoAsignatura
creditos: number
horas_semana: number
}>
}): Promise<Asignatura> {
return invokeEdge<Asignatura>(EDGE.subjects_clone_from_existing, payload);
return invokeEdge<Asignatura>(EDGE.subjects_clone_from_existing, payload)
}
export async function subjects_import_from_file(payload: {
planId: UUID;
archivoWordMateriaId: UUID;
archivosAdicionalesIds?: UUID[];
planId: UUID
archivoWordAsignaturaId: UUID
archivosAdicionalesIds?: UUID[]
}): Promise<Asignatura> {
return invokeEdge<Asignatura>(EDGE.subjects_import_from_file, payload);
return invokeEdge<Asignatura>(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;
codigo: string | null
nombre: string
tipo: TipoAsignatura
creditos: number
horas_semana: number | null
numero_ciclo: number | null
linea_plan_id: UUID | null
datos: Record<string, any>;
}>;
datos: Record<string, any>
}>
export async function subjects_update_fields(subjectId: UUID, patch: SubjectsUpdateFieldsPatch): Promise<Asignatura> {
return invokeEdge<Asignatura>(EDGE.subjects_update_fields, { subjectId, patch });
export async function subjects_update_fields(
subjectId: UUID,
patch: SubjectsUpdateFieldsPatch,
): Promise<Asignatura> {
return invokeEdge<Asignatura>(EDGE.subjects_update_fields, {
subjectId,
patch,
})
}
export async function subjects_update_contenido(subjectId: UUID, unidades: any[]): Promise<Asignatura> {
return invokeEdge<Asignatura>(EDGE.subjects_update_contenido, { subjectId, unidades });
export async function subjects_update_contenido(
subjectId: UUID,
unidades: any[],
): Promise<Asignatura> {
return invokeEdge<Asignatura>(EDGE.subjects_update_contenido, {
subjectId,
unidades,
})
}
export type BibliografiaUpsertInput = Array<{
id?: UUID;
tipo: "BASICA" | "COMPLEMENTARIA";
cita: string;
tipo_fuente?: "MANUAL" | "BIBLIOTECA";
biblioteca_item_id?: string | null;
}>;
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
entries: BibliografiaUpsertInput,
): Promise<{ ok: true }> {
return invokeEdge<{ ok: true }>(EDGE.subjects_update_bibliografia, { subjectId, entries });
return invokeEdge<{ ok: true }>(EDGE.subjects_update_bibliografia, {
subjectId,
entries,
})
}
/** Documento SEP materia */
/** Documento SEP asignatura */
/* export type DocumentoResult = {
archivoId: UUID;
signedUrl: string;
@@ -183,10 +209,18 @@ export async function subjects_update_bibliografia(
nombre?: string;
}; */
export async function subjects_generate_document(subjectId: UUID): Promise<DocumentoResult> {
return invokeEdge<DocumentoResult>(EDGE.subjects_generate_document, { subjectId });
export async function subjects_generate_document(
subjectId: UUID,
): Promise<DocumentoResult> {
return invokeEdge<DocumentoResult>(EDGE.subjects_generate_document, {
subjectId,
})
}
export async function subjects_get_document(subjectId: UUID): Promise<DocumentoResult | null> {
return invokeEdge<DocumentoResult | null>(EDGE.subjects_get_document, { subjectId });
export async function subjects_get_document(
subjectId: UUID,
): Promise<DocumentoResult | null> {
return invokeEdge<DocumentoResult | null>(EDGE.subjects_get_document, {
subjectId,
})
}