feat: add subjects and tasks API, hooks, and related types

- Implemented subjects API with functions for creating, updating, and retrieving subjects, including history and bibliography.
- Added tasks API for managing user tasks, including listing and marking tasks as completed.
- Created hooks for managing AI interactions, authentication, subjects, tasks, and metadata queries.
- Established query keys for caching and managing query states.
- Introduced Supabase client and environment variable management for better configuration.
- Defined types for database and domain models to ensure type safety across the application.
This commit is contained in:
2026-01-09 09:00:33 -06:00
parent 8704b63b46
commit 65a73ca99f
25 changed files with 1819 additions and 0 deletions

45
src/data/api/_helpers.ts Normal file
View File

@@ -0,0 +1,45 @@
import type { PostgrestError, AuthError, SupabaseClient } from "@supabase/supabase-js";
import type { Database } from "../types/database";
export class ApiError extends Error {
constructor(
message: string,
public readonly code?: string,
public readonly details?: unknown,
public readonly hint?: string
) {
super(message);
this.name = "ApiError";
}
}
export function throwIfError(error: PostgrestError | AuthError | null): void {
if (!error) return;
const anyErr = error as any;
throw new ApiError(
anyErr.message ?? "Error inesperado",
anyErr.code,
anyErr.details,
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 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 };
}

81
src/data/api/ai.api.ts Normal file
View File

@@ -0,0 +1,81 @@
import { invokeEdge } from "../supabase/invokeEdge";
import type { InteraccionIA, UUID } from "../types/domain";
const EDGE = {
ai_plan_improve: "ai_plan_improve",
ai_plan_chat: "ai_plan_chat",
ai_subject_improve: "ai_subject_improve",
ai_subject_chat: "ai_subject_chat",
library_search: "library_search",
} as const;
export async function ai_plan_improve(payload: {
planId: UUID;
sectionKey: string; // ej: "perfil_de_egreso" o tu key interna
prompt: string;
context?: Record<string, any>;
fuentes?: {
archivosIds?: UUID[];
vectorStoresIds?: UUID[];
usarMCP?: boolean;
conversacionId?: string;
};
}): Promise<{ interaccion: InteraccionIA; propuesta: any }> {
return invokeEdge<{ interaccion: InteraccionIA; propuesta: any }>(EDGE.ai_plan_improve, payload);
}
export async function ai_plan_chat(payload: {
planId: UUID;
messages: Array<{ role: "system" | "user" | "assistant"; content: string }>;
fuentes?: {
archivosIds?: UUID[];
vectorStoresIds?: UUID[];
usarMCP?: boolean;
conversacionId?: string;
};
}): Promise<{ interaccion: InteraccionIA; reply: string; meta?: any }> {
return invokeEdge<{ interaccion: InteraccionIA; reply: string; meta?: any }>(EDGE.ai_plan_chat, payload);
}
export async function ai_subject_improve(payload: {
subjectId: UUID;
sectionKey: string;
prompt: string;
context?: Record<string, any>;
fuentes?: {
archivosIds?: UUID[];
vectorStoresIds?: UUID[];
usarMCP?: boolean;
conversacionId?: string;
};
}): Promise<{ interaccion: InteraccionIA; propuesta: any }> {
return invokeEdge<{ interaccion: InteraccionIA; propuesta: any }>(EDGE.ai_subject_improve, payload);
}
export async function ai_subject_chat(payload: {
subjectId: UUID;
messages: Array<{ role: "system" | "user" | "assistant"; content: string }>;
fuentes?: {
archivosIds?: UUID[];
vectorStoresIds?: UUID[];
usarMCP?: boolean;
conversacionId?: string;
};
}): Promise<{ interaccion: InteraccionIA; reply: string; meta?: any }> {
return invokeEdge<{ interaccion: InteraccionIA; reply: string; meta?: any }>(EDGE.ai_subject_chat, payload);
}
/** Biblioteca (Edge; adapta a tu API real) */
export type LibraryItem = {
id: string;
titulo: string;
autor?: string;
isbn?: string;
citaSugerida?: string;
disponibilidad?: string;
};
export async function library_search(payload: { query: string; limit?: number }): Promise<LibraryItem[]> {
return invokeEdge<LibraryItem[]>(EDGE.library_search, payload);
}

63
src/data/api/files.api.ts Normal file
View File

@@ -0,0 +1,63 @@
import { supabaseBrowser } from "../supabase/client";
import { throwIfError, requireData, getUserIdOrThrow } from "./_helpers";
import type { Archivo, UUID } from "../types/domain";
const DEFAULT_BUCKET = "archivos";
export type UploadFileInput = {
file: File;
bucket?: string;
pathPrefix?: string; // ej: "planes/<planId>" o "materias/<id>"
temporal?: boolean;
notas?: string | null;
};
export async function files_upload(input: UploadFileInput): Promise<Archivo> {
const supabase = supabaseBrowser();
const userId = await getUserIdOrThrow(supabase);
const bucket = input.bucket ?? DEFAULT_BUCKET;
const safeName = input.file.name.replace(/[^\w.\-() ]+/g, "_");
const pathPrefix = (input.pathPrefix ?? `usuarios/${userId}`).replace(/\/+$/g, "");
const storagePath = `${pathPrefix}/${crypto.randomUUID()}-${safeName}`;
const { data: upData, error: upErr } = await supabase.storage
.from(bucket)
.upload(storagePath, input.file, { upsert: false });
throwIfError(upErr);
requireData(upData, "No se pudo subir archivo.");
const { data: row, error: insErr } = await supabase
.from("archivos")
.insert({
ruta_storage: `${bucket}/${storagePath}`,
nombre: input.file.name,
mime_type: input.file.type || null,
bytes: input.file.size,
subido_por: userId as UUID,
temporal: Boolean(input.temporal),
notas: input.notas ?? null,
})
.select("id,ruta_storage,nombre,mime_type,bytes,subido_por,subido_en,temporal,openai_file_id,notas")
.single();
throwIfError(insErr);
return requireData(row, "No se pudo registrar metadata del archivo.");
}
export async function files_signed_url(params: {
ruta_storage: string; // "bucket/path/to/file"
expiresIn?: number; // segundos
}): Promise<string> {
const supabase = supabaseBrowser();
const expires = params.expiresIn ?? 60 * 10;
const [bucket, ...rest] = params.ruta_storage.split("/");
const path = rest.join("/");
const { data, error } = await supabase.storage.from(bucket).createSignedUrl(path, expires);
throwIfError(error);
return requireData(data?.signedUrl, "No se pudo generar URL firmada.");
}

66
src/data/api/meta.api.ts Normal file
View File

@@ -0,0 +1,66 @@
import { supabaseBrowser } from "../supabase/client";
import { throwIfError } from "./_helpers";
import type { Carrera, EstadoPlan, EstructuraAsignatura, EstructuraPlan, Facultad } from "../types/domain";
export async function facultades_list(): Promise<Facultad[]> {
const supabase = supabaseBrowser();
const { data, error } = await supabase
.from("facultades")
.select("id,nombre,nombre_corto,color,icono,creado_en,actualizado_en")
.order("nombre", { ascending: true });
throwIfError(error);
return data ?? [];
}
export async function carreras_list(params?: { facultadId?: string | null }): Promise<Carrera[]> {
const supabase = supabaseBrowser();
let q = supabase
.from("carreras")
.select(
"id,facultad_id,nombre,nombre_corto,clave_sep,activa,creado_en,actualizado_en, facultades(id,nombre,nombre_corto,color,icono)"
)
.order("nombre", { ascending: true });
if (params?.facultadId) q = q.eq("facultad_id", params.facultadId);
const { data, error } = await q;
throwIfError(error);
return data ?? [];
}
export async function estructuras_plan_list(params?: { nivel?: string | null }): Promise<EstructuraPlan[]> {
const supabase = supabaseBrowser();
// Nota: en tu DDL no hay "nivel" en estructuras_plan; si luego lo agregas, filtra aquí.
const { data, error } = await supabase
.from("estructuras_plan")
.select("id,nombre,tipo,version,definicion")
.order("nombre", { ascending: true });
throwIfError(error);
return data ?? [];
}
export async function estructuras_asignatura_list(): Promise<EstructuraAsignatura[]> {
const supabase = supabaseBrowser();
const { data, error } = await supabase
.from("estructuras_asignatura")
.select("id,nombre,version,definicion")
.order("nombre", { ascending: true });
throwIfError(error);
return data ?? [];
}
export async function estados_plan_list(): Promise<EstadoPlan[]> {
const supabase = supabaseBrowser();
const { data, error } = await supabase
.from("estados_plan")
.select("id,clave,etiqueta,orden,es_final")
.order("orden", { ascending: true });
throwIfError(error);
return data ?? [];
}

View File

@@ -0,0 +1,31 @@
import { supabaseBrowser } from "../supabase/client";
import { throwIfError, getUserIdOrThrow, requireData } from "./_helpers";
import type { Notificacion, UUID } from "../types/domain";
export async function notificaciones_mias_list(): Promise<Notificacion[]> {
const supabase = supabaseBrowser();
const userId = await getUserIdOrThrow(supabase);
const { data, error } = await supabase
.from("notificaciones")
.select("id,usuario_id,tipo,payload,leida,creado_en,leida_en")
.eq("usuario_id", userId as UUID)
.order("creado_en", { ascending: false });
throwIfError(error);
return data ?? [];
}
export async function notificaciones_marcar_leida(notificacionId: UUID): Promise<Notificacion> {
const supabase = supabaseBrowser();
const { data, error } = await supabase
.from("notificaciones")
.update({ leida: true, leida_en: new Date().toISOString() })
.eq("id", notificacionId)
.select("id,usuario_id,tipo,payload,leida,creado_en,leida_en")
.single();
throwIfError(error);
return requireData(data, "No se pudo marcar notificación.");
}

260
src/data/api/plans.api.ts Normal file
View File

@@ -0,0 +1,260 @@
import { supabaseBrowser } from "../supabase/client";
import { invokeEdge } from "../supabase/invokeEdge";
import { buildRange, throwIfError, requireData } from "./_helpers";
import type {
Asignatura,
CambioPlan,
LineaPlan,
NivelPlanEstudio,
Paged,
PlanDatosSep,
PlanEstudio,
TipoCiclo,
UUID,
} from "../types/domain";
const EDGE = {
plans_create_manual: "plans_create_manual",
ai_generate_plan: "ai_generate_plan",
plans_persist_from_ai: "plans_persist_from_ai",
plans_clone_from_existing: "plans_clone_from_existing",
plans_import_from_files: "plans_import_from_files",
plans_update_fields: "plans_update_fields",
plans_update_map: "plans_update_map",
plans_transition_state: "plans_transition_state",
plans_generate_document: "plans_generate_document",
plans_get_document: "plans_get_document",
} as const;
export type PlanListFilters = {
search?: string;
carreraId?: UUID;
facultadId?: UUID; // filtra por carreras.facultad_id
estadoId?: UUID;
activo?: boolean;
limit?: number;
offset?: number;
};
export async function plans_list(filters: PlanListFilters = {}): Promise<Paged<PlanEstudio>> {
const supabase = supabaseBrowser();
let q = supabase
.from("planes_estudio")
.select(
`
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_plan(id,nombre,tipo,version,definicion),
estados_plan(id,clave,etiqueta,orden,es_final)
`,
{ count: "exact" }
)
.order("actualizado_en", { ascending: false });
if (filters.search?.trim()) q = q.ilike("nombre", `%${filters.search.trim()}%`);
if (filters.carreraId) q = q.eq("carrera_id", filters.carreraId);
if (filters.estadoId) q = q.eq("estado_actual_id", filters.estadoId);
if (typeof filters.activo === "boolean") q = q.eq("activo", filters.activo);
// filtro por FK “hacia arriba” (PostgREST soporta filtros en recursos embebidos)
if (filters.facultadId) q = q.eq("carreras.facultad_id", filters.facultadId);
const { from, to } = buildRange(filters.limit, filters.offset);
if (typeof from === "number" && typeof to === "number") q = q.range(from, to);
const { data, error, count } = await q;
throwIfError(error);
return { data: data ?? [], count: count ?? null };
}
export async function plans_get(planId: UUID): Promise<PlanEstudio> {
const supabase = supabaseBrowser();
const { data, error } = await supabase
.from("planes_estudio")
.select(
`
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_plan(id,nombre,tipo,version,definicion),
estados_plan(id,clave,etiqueta,orden,es_final)
`
)
.eq("id", planId)
.single();
throwIfError(error);
return requireData(data, "Plan no encontrado.");
}
export async function plan_lineas_list(planId: UUID): Promise<LineaPlan[]> {
const supabase = supabaseBrowser();
const { data, error } = await supabase
.from("lineas_plan")
.select("id,plan_estudio_id,nombre,orden,area,creado_en,actualizado_en")
.eq("plan_estudio_id", planId)
.order("orden", { ascending: true });
throwIfError(error);
return data ?? [];
}
export async function plan_asignaturas_list(planId: UUID): Promise<Asignatura[]> {
const supabase = supabaseBrowser();
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"
)
.eq("plan_estudio_id", planId)
.order("numero_ciclo", { ascending: true, nullsFirst: false })
.order("orden_celda", { ascending: true, nullsFirst: false })
.order("nombre", { ascending: true });
throwIfError(error);
return data ?? [];
}
export async function plans_history(planId: UUID): Promise<CambioPlan[]> {
const supabase = supabaseBrowser();
const { data, error } = await supabase
.from("cambios_plan")
.select("id,plan_estudio_id,cambiado_por,cambiado_en,tipo,campo,valor_anterior,valor_nuevo,interaccion_ia_id")
.eq("plan_estudio_id", planId)
.order("cambiado_en", { ascending: false });
throwIfError(error);
return data ?? [];
}
/** Wizard: crear plan manual (Edge Function) */
export type PlansCreateManualInput = {
carreraId: UUID;
estructuraId: UUID;
nombre: string;
nivel: NivelPlanEstudio;
tipoCiclo: TipoCiclo;
numCiclos: number;
datos?: Partial<PlanDatosSep> & Record<string, any>;
};
export async function plans_create_manual(input: PlansCreateManualInput): Promise<PlanEstudio> {
return invokeEdge<PlanEstudio>(EDGE.plans_create_manual, input);
}
/** Wizard: IA genera preview JSON (Edge Function) */
export type AIGeneratePlanInput = {
datosBasicos: {
nombrePlan: string;
carreraId: UUID;
facultadId?: UUID;
nivel: string;
tipoCiclo: TipoCiclo;
numCiclos: number;
};
iaConfig: {
descripcionEnfoque: string;
poblacionObjetivo?: string;
notasAdicionales?: string;
archivosReferencia?: UUID[];
repositoriosIds?: UUID[];
usarMCP?: boolean;
};
};
export async function ai_generate_plan(input: AIGeneratePlanInput): Promise<any> {
return invokeEdge<any>(EDGE.ai_generate_plan, input);
}
export async function plans_persist_from_ai(payload: { jsonPlan: any }): Promise<PlanEstudio> {
return invokeEdge<PlanEstudio>(EDGE.plans_persist_from_ai, payload);
}
export async function plans_clone_from_existing(payload: {
planOrigenId: UUID;
overrides: Partial<Pick<PlanEstudio, "nombre" | "nivel" | "tipo_ciclo" | "numero_ciclos">> & {
carrera_id?: UUID;
estructura_id?: UUID;
datos?: Partial<PlanDatosSep> & Record<string, any>;
};
}): Promise<PlanEstudio> {
return invokeEdge<PlanEstudio>(EDGE.plans_clone_from_existing, payload);
}
export async function plans_import_from_files(payload: {
datosBasicos: {
nombrePlan: string;
carreraId: UUID;
estructuraId: UUID;
nivel: string;
tipoCiclo: TipoCiclo;
numCiclos: number;
};
archivoWordPlanId: UUID;
archivoMapaExcelId?: UUID | null;
archivoMateriasExcelId?: UUID | null;
}): Promise<PlanEstudio> {
return invokeEdge<PlanEstudio>(EDGE.plans_import_from_files, payload);
}
/** Update de tarjetas/fields del plan (Edge Function: merge server-side) */
export type PlansUpdateFieldsPatch = {
nombre?: string;
nivel?: NivelPlanEstudio;
tipo_ciclo?: TipoCiclo;
numero_ciclos?: number;
datos?: Partial<PlanDatosSep> & Record<string, any>;
};
export async function plans_update_fields(planId: UUID, patch: PlansUpdateFieldsPatch): Promise<PlanEstudio> {
return invokeEdge<PlanEstudio>(EDGE.plans_update_fields, { planId, patch });
}
/** Operaciones del mapa curricular (mover/reordenar) */
export type PlanMapOperation =
| {
op: "MOVE_ASIGNATURA";
asignaturaId: UUID;
numero_ciclo: number | null;
linea_plan_id: UUID | null;
orden_celda?: number | null;
}
| {
op: "REORDER_CELDA";
linea_plan_id: UUID;
numero_ciclo: number;
asignaturaIdsOrdenados: UUID[];
};
export async function plans_update_map(planId: UUID, ops: PlanMapOperation[]): Promise<{ ok: true }> {
return invokeEdge<{ ok: true }>(EDGE.plans_update_map, { planId, ops });
}
export async function plans_transition_state(payload: {
planId: UUID;
haciaEstadoId: UUID;
comentario?: string;
}): Promise<{ ok: true }> {
return invokeEdge<{ ok: true }>(EDGE.plans_transition_state, payload);
}
/** Documento (Edge Function: genera y devuelve URL firmada o metadata) */
export type DocumentoResult = {
archivoId: UUID;
signedUrl: string;
mimeType?: string;
nombre?: string;
};
export async function plans_generate_document(planId: UUID): Promise<DocumentoResult> {
return invokeEdge<DocumentoResult>(EDGE.plans_generate_document, { planId });
}
export async function plans_get_document(planId: UUID): Promise<DocumentoResult | null> {
return invokeEdge<DocumentoResult | null>(EDGE.plans_get_document, { planId });
}

View File

@@ -0,0 +1,192 @@
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";
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_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;
export async function subjects_get(subjectId: UUID): Promise<Asignatura> {
const supabase = supabaseBrowser();
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,
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, "Materia no encontrada.");
}
export async function subjects_history(subjectId: UUID): Promise<CambioAsignatura[]> {
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<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 });
throwIfError(error);
return data ?? [];
}
/** Wizard: crear materia manual (Edge Function) */
export type SubjectsCreateManualInput = {
planId: UUID;
datosBasicos: {
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 ai_generate_subject(payload: {
planId: UUID;
datosBasicos: {
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;
};
}): Promise<any> {
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_clone_from_existing(payload: {
materiaOrigenId: UUID;
planDestinoId: UUID;
overrides?: Partial<{
nombre: string;
codigo: string;
tipo: TipoAsignatura;
creditos: number;
horas_semana: number;
}>;
}): Promise<Asignatura> {
return invokeEdge<Asignatura>(EDGE.subjects_clone_from_existing, payload);
}
export async function subjects_import_from_file(payload: {
planId: UUID;
archivoWordMateriaId: UUID;
archivosAdicionalesIds?: UUID[];
}): Promise<Asignatura> {
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;
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_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;
}>;
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 materia */
/* export type DocumentoResult = {
archivoId: UUID;
signedUrl: string;
mimeType?: string;
nombre?: string;
}; */
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 });
}

31
src/data/api/tasks.api.ts Normal file
View File

@@ -0,0 +1,31 @@
import { supabaseBrowser } from "../supabase/client";
import { throwIfError, getUserIdOrThrow, requireData } from "./_helpers";
import type { TareaRevision, UUID } from "../types/domain";
export async function tareas_mias_list(): Promise<TareaRevision[]> {
const supabase = supabaseBrowser();
const userId = await getUserIdOrThrow(supabase);
const { data, error } = await supabase
.from("tareas_revision")
.select("id,plan_estudio_id,asignado_a,rol_id,estado_id,estatus,fecha_limite,creado_en,completado_en")
.eq("asignado_a", userId as UUID)
.order("creado_en", { ascending: false });
throwIfError(error);
return data ?? [];
}
export async function tareas_marcar_completada(tareaId: UUID): Promise<TareaRevision> {
const supabase = supabaseBrowser();
const { data, error } = await supabase
.from("tareas_revision")
.update({ estatus: "COMPLETADA", completado_en: new Date().toISOString() })
.eq("id", tareaId)
.select("id,plan_estudio_id,asignado_a,rol_id,estado_id,estatus,fecha_limite,creado_en,completado_en")
.single();
throwIfError(error);
return requireData(data, "No se pudo marcar tarea.");
}