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:
45
src/data/api/_helpers.ts
Normal file
45
src/data/api/_helpers.ts
Normal 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
81
src/data/api/ai.api.ts
Normal 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
63
src/data/api/files.api.ts
Normal 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
66
src/data/api/meta.api.ts
Normal 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 ?? [];
|
||||
}
|
||||
31
src/data/api/notifications.api.ts
Normal file
31
src/data/api/notifications.api.ts
Normal 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
260
src/data/api/plans.api.ts
Normal 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 });
|
||||
}
|
||||
192
src/data/api/subjects.api.ts
Normal file
192
src/data/api/subjects.api.ts
Normal 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
31
src/data/api/tasks.api.ts
Normal 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.");
|
||||
}
|
||||
Reference in New Issue
Block a user