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:
19
bun.lock
19
bun.lock
@@ -15,6 +15,7 @@
|
|||||||
"@radix-ui/react-tabs": "^1.1.13",
|
"@radix-ui/react-tabs": "^1.1.13",
|
||||||
"@radix-ui/react-tooltip": "^1.2.8",
|
"@radix-ui/react-tooltip": "^1.2.8",
|
||||||
"@stepperize/react": "^5.1.9",
|
"@stepperize/react": "^5.1.9",
|
||||||
|
"@supabase/supabase-js": "^2.90.1",
|
||||||
"@tailwindcss/vite": "^4.0.6",
|
"@tailwindcss/vite": "^4.0.6",
|
||||||
"@tanstack/react-devtools": "^0.7.0",
|
"@tanstack/react-devtools": "^0.7.0",
|
||||||
"@tanstack/react-query": "^5.66.5",
|
"@tanstack/react-query": "^5.66.5",
|
||||||
@@ -377,6 +378,18 @@
|
|||||||
|
|
||||||
"@stylistic/eslint-plugin": ["@stylistic/eslint-plugin@5.6.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.0", "@typescript-eslint/types": "^8.47.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "estraverse": "^5.3.0", "picomatch": "^4.0.3" }, "peerDependencies": { "eslint": ">=9.0.0" } }, "sha512-JCs+MqoXfXrRPGbGmho/zGS/jMcn3ieKl/A8YImqib76C8kjgZwq5uUFzc30lJkMvcchuRn6/v8IApLxli3Jyw=="],
|
"@stylistic/eslint-plugin": ["@stylistic/eslint-plugin@5.6.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.0", "@typescript-eslint/types": "^8.47.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "estraverse": "^5.3.0", "picomatch": "^4.0.3" }, "peerDependencies": { "eslint": ">=9.0.0" } }, "sha512-JCs+MqoXfXrRPGbGmho/zGS/jMcn3ieKl/A8YImqib76C8kjgZwq5uUFzc30lJkMvcchuRn6/v8IApLxli3Jyw=="],
|
||||||
|
|
||||||
|
"@supabase/auth-js": ["@supabase/auth-js@2.90.1", "", { "dependencies": { "tslib": "2.8.1" } }, "sha512-vxb66dgo6h3yyPbR06735Ps+dK3hj0JwS8w9fdQPVZQmocSTlKUW5MfxSy99mN0XqCCuLMQ3jCEiIIUU23e9ng=="],
|
||||||
|
|
||||||
|
"@supabase/functions-js": ["@supabase/functions-js@2.90.1", "", { "dependencies": { "tslib": "2.8.1" } }, "sha512-x9mV9dF1Lam9qL3zlpP6mSM5C9iqMPtF5B/tU1Jj/F0ufX5mjDf9ghVBaErVxmrQJRL4+iMKWKY2GnODkpS8tw=="],
|
||||||
|
|
||||||
|
"@supabase/postgrest-js": ["@supabase/postgrest-js@2.90.1", "", { "dependencies": { "tslib": "2.8.1" } }, "sha512-jh6vqzaYzoFn3raaC0hcFt9h+Bt+uxNRBSdc7PfToQeRGk7PDPoweHsbdiPWREtDVTGKfu+PyPW9e2jbK+BCgQ=="],
|
||||||
|
|
||||||
|
"@supabase/realtime-js": ["@supabase/realtime-js@2.90.1", "", { "dependencies": { "@types/phoenix": "^1.6.6", "@types/ws": "^8.18.1", "tslib": "2.8.1", "ws": "^8.18.2" } }, "sha512-PWbnEMkcQRuor8jhObp4+Snufkq8C6fBp+MchVp2qBPY1NXk/c3Iv3YyiFYVzo0Dzuw4nAlT4+ahuPggy4r32w=="],
|
||||||
|
|
||||||
|
"@supabase/storage-js": ["@supabase/storage-js@2.90.1", "", { "dependencies": { "iceberg-js": "^0.8.1", "tslib": "2.8.1" } }, "sha512-GHY+Ps/K/RBfRj7kwx+iVf2HIdqOS43rM2iDOIDpapyUnGA9CCBFzFV/XvfzznGykd//z2dkGZhlZZprsVFqGg=="],
|
||||||
|
|
||||||
|
"@supabase/supabase-js": ["@supabase/supabase-js@2.90.1", "", { "dependencies": { "@supabase/auth-js": "2.90.1", "@supabase/functions-js": "2.90.1", "@supabase/postgrest-js": "2.90.1", "@supabase/realtime-js": "2.90.1", "@supabase/storage-js": "2.90.1" } }, "sha512-U8KaKGLUgTIFHtwEW1dgw1gK7XrdpvvYo7nzzqPx721GqPe8WZbAiLh/hmyKLGBYQ/mmQNr20vU9tWSDZpii3w=="],
|
||||||
|
|
||||||
"@tailwindcss/node": ["@tailwindcss/node@4.1.18", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.6.1", "lightningcss": "1.30.2", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.1.18" } }, "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ=="],
|
"@tailwindcss/node": ["@tailwindcss/node@4.1.18", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.6.1", "lightningcss": "1.30.2", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.1.18" } }, "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.18", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.18", "@tailwindcss/oxide-darwin-arm64": "4.1.18", "@tailwindcss/oxide-darwin-x64": "4.1.18", "@tailwindcss/oxide-freebsd-x64": "4.1.18", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.18", "@tailwindcss/oxide-linux-arm64-musl": "4.1.18", "@tailwindcss/oxide-linux-x64-gnu": "4.1.18", "@tailwindcss/oxide-linux-x64-musl": "4.1.18", "@tailwindcss/oxide-wasm32-wasi": "4.1.18", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.18", "@tailwindcss/oxide-win32-x64-msvc": "4.1.18" } }, "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A=="],
|
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.18", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.18", "@tailwindcss/oxide-darwin-arm64": "4.1.18", "@tailwindcss/oxide-darwin-x64": "4.1.18", "@tailwindcss/oxide-freebsd-x64": "4.1.18", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.18", "@tailwindcss/oxide-linux-arm64-musl": "4.1.18", "@tailwindcss/oxide-linux-x64-gnu": "4.1.18", "@tailwindcss/oxide-linux-x64-musl": "4.1.18", "@tailwindcss/oxide-wasm32-wasi": "4.1.18", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.18", "@tailwindcss/oxide-win32-x64-msvc": "4.1.18" } }, "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A=="],
|
||||||
@@ -481,10 +494,14 @@
|
|||||||
|
|
||||||
"@types/node": ["@types/node@22.19.3", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA=="],
|
"@types/node": ["@types/node@22.19.3", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA=="],
|
||||||
|
|
||||||
|
"@types/phoenix": ["@types/phoenix@1.6.7", "", {}, "sha512-oN9ive//QSBkf19rfDv45M7eZPi0eEXylht2OLEXicu5b4KoQ1OzXIw+xDSGWxSxe1JmepRR/ZH283vsu518/Q=="],
|
||||||
|
|
||||||
"@types/react": ["@types/react@19.2.7", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg=="],
|
"@types/react": ["@types/react@19.2.7", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg=="],
|
||||||
|
|
||||||
"@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
|
"@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
|
||||||
|
|
||||||
|
"@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],
|
||||||
|
|
||||||
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.52.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.52.0", "@typescript-eslint/type-utils": "8.52.0", "@typescript-eslint/utils": "8.52.0", "@typescript-eslint/visitor-keys": "8.52.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.52.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-okqtOgqu2qmZJ5iN4TWlgfF171dZmx2FzdOv2K/ixL2LZWDStL8+JgQerI2sa8eAEfoydG9+0V96m7V+P8yE1Q=="],
|
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.52.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.52.0", "@typescript-eslint/type-utils": "8.52.0", "@typescript-eslint/utils": "8.52.0", "@typescript-eslint/visitor-keys": "8.52.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.52.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-okqtOgqu2qmZJ5iN4TWlgfF171dZmx2FzdOv2K/ixL2LZWDStL8+JgQerI2sa8eAEfoydG9+0V96m7V+P8yE1Q=="],
|
||||||
|
|
||||||
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.52.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.52.0", "@typescript-eslint/types": "8.52.0", "@typescript-eslint/typescript-estree": "8.52.0", "@typescript-eslint/visitor-keys": "8.52.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-iIACsx8pxRnguSYhHiMn2PvhvfpopO9FXHyn1mG5txZIsAaB6F0KwbFnUQN3KCiG3Jcuad/Cao2FAs1Wp7vAyg=="],
|
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.52.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.52.0", "@typescript-eslint/types": "8.52.0", "@typescript-eslint/typescript-estree": "8.52.0", "@typescript-eslint/visitor-keys": "8.52.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-iIACsx8pxRnguSYhHiMn2PvhvfpopO9FXHyn1mG5txZIsAaB6F0KwbFnUQN3KCiG3Jcuad/Cao2FAs1Wp7vAyg=="],
|
||||||
@@ -857,6 +874,8 @@
|
|||||||
|
|
||||||
"https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
|
"https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
|
||||||
|
|
||||||
|
"iceberg-js": ["iceberg-js@0.8.1", "", {}, "sha512-1dhVQZXhcHje7798IVM+xoo/1ZdVfzOMIc8/rgVSijRK38EDqOJoGula9N/8ZI5RD8QTxNQtK/Gozpr+qUqRRA=="],
|
||||||
|
|
||||||
"ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
|
"ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
|
||||||
|
|
||||||
"import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
|
"import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
|
||||||
|
|||||||
@@ -28,6 +28,7 @@
|
|||||||
"@radix-ui/react-tabs": "^1.1.13",
|
"@radix-ui/react-tabs": "^1.1.13",
|
||||||
"@radix-ui/react-tooltip": "^1.2.8",
|
"@radix-ui/react-tooltip": "^1.2.8",
|
||||||
"@stepperize/react": "^5.1.9",
|
"@stepperize/react": "^5.1.9",
|
||||||
|
"@supabase/supabase-js": "^2.90.1",
|
||||||
"@tailwindcss/vite": "^4.0.6",
|
"@tailwindcss/vite": "^4.0.6",
|
||||||
"@tanstack/react-devtools": "^0.7.0",
|
"@tanstack/react-devtools": "^0.7.0",
|
||||||
"@tanstack/react-query": "^5.66.5",
|
"@tanstack/react-query": "^5.66.5",
|
||||||
|
|||||||
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.");
|
||||||
|
}
|
||||||
28
src/data/hooks/useAI.ts
Normal file
28
src/data/hooks/useAI.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { useMutation } from "@tanstack/react-query";
|
||||||
|
import {
|
||||||
|
ai_plan_chat,
|
||||||
|
ai_plan_improve,
|
||||||
|
ai_subject_chat,
|
||||||
|
ai_subject_improve,
|
||||||
|
library_search,
|
||||||
|
} from "../api/ai.api";
|
||||||
|
|
||||||
|
export function useAIPlanImprove() {
|
||||||
|
return useMutation({ mutationFn: ai_plan_improve });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useAIPlanChat() {
|
||||||
|
return useMutation({ mutationFn: ai_plan_chat });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useAISubjectImprove() {
|
||||||
|
return useMutation({ mutationFn: ai_subject_improve });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useAISubjectChat() {
|
||||||
|
return useMutation({ mutationFn: ai_subject_chat });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useLibrarySearch() {
|
||||||
|
return useMutation({ mutationFn: library_search });
|
||||||
|
}
|
||||||
59
src/data/hooks/useAuth.ts
Normal file
59
src/data/hooks/useAuth.ts
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { supabaseBrowser } from "../supabase/client";
|
||||||
|
import { qk } from "../query/keys";
|
||||||
|
import { throwIfError } from "../api/_helpers";
|
||||||
|
|
||||||
|
export function useSession() {
|
||||||
|
const supabase = supabaseBrowser();
|
||||||
|
const qc = useQueryClient();
|
||||||
|
|
||||||
|
const query = useQuery({
|
||||||
|
queryKey: qk.session(),
|
||||||
|
queryFn: async () => {
|
||||||
|
const { data, error } = await supabase.auth.getSession();
|
||||||
|
throwIfError(error);
|
||||||
|
return data.session ?? null;
|
||||||
|
},
|
||||||
|
staleTime: Infinity,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const { data } = supabase.auth.onAuthStateChange(() => {
|
||||||
|
qc.invalidateQueries({ queryKey: qk.session() });
|
||||||
|
qc.invalidateQueries({ queryKey: qk.meProfile() });
|
||||||
|
qc.invalidateQueries({ queryKey: qk.auth });
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => data.subscription.unsubscribe();
|
||||||
|
}, [supabase, qc]);
|
||||||
|
|
||||||
|
return query;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useMeProfile() {
|
||||||
|
const supabase = supabaseBrowser();
|
||||||
|
|
||||||
|
return useQuery({
|
||||||
|
queryKey: qk.meProfile(),
|
||||||
|
queryFn: async () => {
|
||||||
|
const { data: u, error: uErr } = await supabase.auth.getUser();
|
||||||
|
throwIfError(uErr);
|
||||||
|
const userId = u.user?.id;
|
||||||
|
if (!userId) return null;
|
||||||
|
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from("usuarios_app")
|
||||||
|
.select("id,nombre_completo,email,externo,creado_en,actualizado_en")
|
||||||
|
.eq("id", userId)
|
||||||
|
.single();
|
||||||
|
|
||||||
|
// si aún no existe perfil en usuarios_app, permite null (tu seed/trigger puede crearlo)
|
||||||
|
if (error && (error as any).code === "PGRST116") return null;
|
||||||
|
|
||||||
|
throwIfError(error);
|
||||||
|
return data ?? null;
|
||||||
|
},
|
||||||
|
staleTime: 60_000,
|
||||||
|
});
|
||||||
|
}
|
||||||
49
src/data/hooks/useMeta.ts
Normal file
49
src/data/hooks/useMeta.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { qk } from "../query/keys";
|
||||||
|
import {
|
||||||
|
carreras_list,
|
||||||
|
estados_plan_list,
|
||||||
|
estructuras_asignatura_list,
|
||||||
|
estructuras_plan_list,
|
||||||
|
facultades_list,
|
||||||
|
} from "../api/meta.api";
|
||||||
|
|
||||||
|
export function useFacultades() {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: qk.facultades(),
|
||||||
|
queryFn: facultades_list,
|
||||||
|
staleTime: 5 * 60_000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useCarreras(params?: { facultadId?: string | null }) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: qk.carreras(params?.facultadId ?? null),
|
||||||
|
queryFn: () => carreras_list(params),
|
||||||
|
staleTime: 5 * 60_000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useEstructurasPlan(params?: { nivel?: string | null }) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: qk.estructurasPlan(params?.nivel ?? null),
|
||||||
|
queryFn: () => estructuras_plan_list(params),
|
||||||
|
staleTime: 10 * 60_000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useEstructurasAsignatura() {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: qk.estructurasAsignatura(),
|
||||||
|
queryFn: estructuras_asignatura_list,
|
||||||
|
staleTime: 10 * 60_000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useEstadosPlan() {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: qk.estadosPlan(),
|
||||||
|
queryFn: estados_plan_list,
|
||||||
|
staleTime: 10 * 60_000,
|
||||||
|
});
|
||||||
|
}
|
||||||
49
src/data/hooks/useNotifications.ts
Normal file
49
src/data/hooks/useNotifications.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { qk } from "../query/keys";
|
||||||
|
import { notificaciones_marcar_leida, notificaciones_mias_list } from "../api/notifications.api";
|
||||||
|
import { supabaseBrowser } from "../supabase/client";
|
||||||
|
|
||||||
|
export function useMisNotificaciones() {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: qk.notificaciones(),
|
||||||
|
queryFn: notificaciones_mias_list,
|
||||||
|
staleTime: 10_000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 🔥 Opcional: realtime (si tienes Realtime habilitado) */
|
||||||
|
export function useRealtimeNotificaciones(enable = true) {
|
||||||
|
const supabase = supabaseBrowser();
|
||||||
|
const qc = useQueryClient();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!enable) return;
|
||||||
|
|
||||||
|
const channel = supabase
|
||||||
|
.channel("rt-notificaciones")
|
||||||
|
.on(
|
||||||
|
"postgres_changes",
|
||||||
|
{ event: "*", schema: "public", table: "notificaciones" },
|
||||||
|
() => {
|
||||||
|
qc.invalidateQueries({ queryKey: qk.notificaciones() });
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.subscribe();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
supabase.removeChannel(channel);
|
||||||
|
};
|
||||||
|
}, [enable, supabase, qc]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useMarcarNotificacionLeida() {
|
||||||
|
const qc = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: notificaciones_marcar_leida,
|
||||||
|
onSuccess: () => {
|
||||||
|
qc.invalidateQueries({ queryKey: qk.notificaciones() });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
210
src/data/hooks/usePlans.ts
Normal file
210
src/data/hooks/usePlans.ts
Normal file
@@ -0,0 +1,210 @@
|
|||||||
|
import { keepPreviousData, useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { qk } from "../query/keys";
|
||||||
|
import type { PlanEstudio, UUID } from "../types/domain";
|
||||||
|
import type { PlanListFilters, PlanMapOperation, PlansCreateManualInput, PlansUpdateFieldsPatch } from "../api/plans.api";
|
||||||
|
import {
|
||||||
|
ai_generate_plan,
|
||||||
|
plan_asignaturas_list,
|
||||||
|
plan_lineas_list,
|
||||||
|
plans_clone_from_existing,
|
||||||
|
plans_create_manual,
|
||||||
|
plans_generate_document,
|
||||||
|
plans_get,
|
||||||
|
plans_get_document,
|
||||||
|
plans_history,
|
||||||
|
plans_import_from_files,
|
||||||
|
plans_list,
|
||||||
|
plans_persist_from_ai,
|
||||||
|
plans_transition_state,
|
||||||
|
plans_update_fields,
|
||||||
|
plans_update_map,
|
||||||
|
} from "../api/plans.api";
|
||||||
|
|
||||||
|
export function usePlanes(filters: PlanListFilters) {
|
||||||
|
// 🧠 Tip: memoiza "filters" (useMemo) para que queryKey sea estable.
|
||||||
|
return useQuery({
|
||||||
|
queryKey: qk.planesList(filters),
|
||||||
|
queryFn: () => plans_list(filters),
|
||||||
|
placeholderData: keepPreviousData,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function usePlan(planId: UUID | null | undefined) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: planId ? qk.plan(planId) : ["planes", "detail", null],
|
||||||
|
queryFn: () => plans_get(planId as UUID),
|
||||||
|
enabled: Boolean(planId),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function usePlanLineas(planId: UUID | null | undefined) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: planId ? qk.planLineas(planId) : ["planes", "lineas", null],
|
||||||
|
queryFn: () => plan_lineas_list(planId as UUID),
|
||||||
|
enabled: Boolean(planId),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function usePlanAsignaturas(planId: UUID | null | undefined) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: planId ? qk.planAsignaturas(planId) : ["planes", "asignaturas", null],
|
||||||
|
queryFn: () => plan_asignaturas_list(planId as UUID),
|
||||||
|
enabled: Boolean(planId),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function usePlanHistorial(planId: UUID | null | undefined) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: planId ? qk.planHistorial(planId) : ["planes", "historial", null],
|
||||||
|
queryFn: () => plans_history(planId as UUID),
|
||||||
|
enabled: Boolean(planId),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function usePlanDocumento(planId: UUID | null | undefined) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: planId ? qk.planDocumento(planId) : ["planes", "documento", null],
|
||||||
|
queryFn: () => plans_get_document(planId as UUID),
|
||||||
|
enabled: Boolean(planId),
|
||||||
|
staleTime: 30_000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------ Mutations ------------------ */
|
||||||
|
|
||||||
|
export function useCreatePlanManual() {
|
||||||
|
const qc = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (input: PlansCreateManualInput) => plans_create_manual(input),
|
||||||
|
onSuccess: (plan) => {
|
||||||
|
qc.invalidateQueries({ queryKey: ["planes", "list"] });
|
||||||
|
qc.setQueryData(qk.plan(plan.id), plan);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useGeneratePlanAI() {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: ai_generate_plan,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function usePersistPlanFromAI() {
|
||||||
|
const qc = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (payload: { jsonPlan: any }) => plans_persist_from_ai(payload),
|
||||||
|
onSuccess: (plan) => {
|
||||||
|
qc.invalidateQueries({ queryKey: ["planes", "list"] });
|
||||||
|
qc.setQueryData(qk.plan(plan.id), plan);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useClonePlan() {
|
||||||
|
const qc = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: plans_clone_from_existing,
|
||||||
|
onSuccess: (plan) => {
|
||||||
|
qc.invalidateQueries({ queryKey: ["planes", "list"] });
|
||||||
|
qc.setQueryData(qk.plan(plan.id), plan);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useImportPlanFromFiles() {
|
||||||
|
const qc = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: plans_import_from_files,
|
||||||
|
onSuccess: (plan) => {
|
||||||
|
qc.invalidateQueries({ queryKey: ["planes", "list"] });
|
||||||
|
qc.setQueryData(qk.plan(plan.id), plan);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useUpdatePlanFields() {
|
||||||
|
const qc = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (vars: { planId: UUID; patch: PlansUpdateFieldsPatch }) =>
|
||||||
|
plans_update_fields(vars.planId, vars.patch),
|
||||||
|
onSuccess: (updated) => {
|
||||||
|
qc.setQueryData(qk.plan(updated.id), updated);
|
||||||
|
qc.invalidateQueries({ queryKey: ["planes", "list"] });
|
||||||
|
qc.invalidateQueries({ queryKey: qk.planHistorial(updated.id) });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useUpdatePlanMapa() {
|
||||||
|
const qc = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (vars: { planId: UUID; ops: PlanMapOperation[] }) => plans_update_map(vars.planId, vars.ops),
|
||||||
|
|
||||||
|
// ✅ Optimista (rápida) para el caso MOVE_ASIGNATURA
|
||||||
|
onMutate: async (vars) => {
|
||||||
|
await qc.cancelQueries({ queryKey: qk.planAsignaturas(vars.planId) });
|
||||||
|
const prev = qc.getQueryData<any>(qk.planAsignaturas(vars.planId));
|
||||||
|
|
||||||
|
// solo optimizamos MOVEs simples
|
||||||
|
const moves = vars.ops.filter((x) => x.op === "MOVE_ASIGNATURA") as Array<
|
||||||
|
Extract<PlanMapOperation, { op: "MOVE_ASIGNATURA" }>
|
||||||
|
>;
|
||||||
|
|
||||||
|
if (prev && Array.isArray(prev) && moves.length) {
|
||||||
|
const next = prev.map((a: any) => {
|
||||||
|
const m = moves.find((x) => x.asignaturaId === a.id);
|
||||||
|
if (!m) return a;
|
||||||
|
return {
|
||||||
|
...a,
|
||||||
|
numero_ciclo: m.numero_ciclo,
|
||||||
|
linea_plan_id: m.linea_plan_id,
|
||||||
|
orden_celda: m.orden_celda ?? a.orden_celda,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
qc.setQueryData(qk.planAsignaturas(vars.planId), next);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { prev };
|
||||||
|
},
|
||||||
|
|
||||||
|
onError: (_err, vars, ctx) => {
|
||||||
|
if (ctx?.prev) qc.setQueryData(qk.planAsignaturas(vars.planId), ctx.prev);
|
||||||
|
},
|
||||||
|
|
||||||
|
onSuccess: (_ok, vars) => {
|
||||||
|
qc.invalidateQueries({ queryKey: qk.planAsignaturas(vars.planId) });
|
||||||
|
qc.invalidateQueries({ queryKey: qk.planHistorial(vars.planId) });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useTransitionPlanEstado() {
|
||||||
|
const qc = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: plans_transition_state,
|
||||||
|
onSuccess: (_ok, vars) => {
|
||||||
|
qc.invalidateQueries({ queryKey: qk.plan(vars.planId) });
|
||||||
|
qc.invalidateQueries({ queryKey: qk.planHistorial(vars.planId) });
|
||||||
|
qc.invalidateQueries({ queryKey: ["planes", "list"] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useGeneratePlanDocumento() {
|
||||||
|
const qc = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (planId: UUID) => plans_generate_document(planId),
|
||||||
|
onSuccess: (_doc, planId) => {
|
||||||
|
qc.invalidateQueries({ queryKey: qk.planDocumento(planId) });
|
||||||
|
qc.invalidateQueries({ queryKey: qk.planHistorial(planId) });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
166
src/data/hooks/useSubjects.ts
Normal file
166
src/data/hooks/useSubjects.ts
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
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,
|
||||||
|
subjects_bibliografia_list,
|
||||||
|
subjects_clone_from_existing,
|
||||||
|
subjects_create_manual,
|
||||||
|
subjects_generate_document,
|
||||||
|
subjects_get,
|
||||||
|
subjects_get_document,
|
||||||
|
subjects_history,
|
||||||
|
subjects_import_from_file,
|
||||||
|
subjects_persist_from_ai,
|
||||||
|
subjects_update_bibliografia,
|
||||||
|
subjects_update_contenido,
|
||||||
|
subjects_update_fields,
|
||||||
|
} from "../api/subjects.api";
|
||||||
|
|
||||||
|
export function useSubject(subjectId: UUID | null | undefined) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: subjectId ? qk.asignatura(subjectId) : ["asignaturas", "detail", null],
|
||||||
|
queryFn: () => subjects_get(subjectId as UUID),
|
||||||
|
enabled: Boolean(subjectId),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSubjectBibliografia(subjectId: UUID | null | undefined) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: subjectId ? qk.asignaturaBibliografia(subjectId) : ["asignaturas", "bibliografia", null],
|
||||||
|
queryFn: () => subjects_bibliografia_list(subjectId as UUID),
|
||||||
|
enabled: Boolean(subjectId),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSubjectHistorial(subjectId: UUID | null | undefined) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: subjectId ? qk.asignaturaHistorial(subjectId) : ["asignaturas", "historial", null],
|
||||||
|
queryFn: () => subjects_history(subjectId as UUID),
|
||||||
|
enabled: Boolean(subjectId),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSubjectDocumento(subjectId: UUID | null | undefined) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: subjectId ? qk.asignaturaDocumento(subjectId) : ["asignaturas", "documento", null],
|
||||||
|
queryFn: () => subjects_get_document(subjectId as UUID),
|
||||||
|
enabled: Boolean(subjectId),
|
||||||
|
staleTime: 30_000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------ Mutations ------------------ */
|
||||||
|
|
||||||
|
export function useCreateSubjectManual() {
|
||||||
|
const qc = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (payload: SubjectsCreateManualInput) => subjects_create_manual(payload),
|
||||||
|
onSuccess: (subject) => {
|
||||||
|
qc.setQueryData(qk.asignatura(subject.id), subject);
|
||||||
|
qc.invalidateQueries({ queryKey: qk.planAsignaturas(subject.plan_estudio_id) });
|
||||||
|
qc.invalidateQueries({ queryKey: qk.planHistorial(subject.plan_estudio_id) });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useGenerateSubjectAI() {
|
||||||
|
return useMutation({ mutationFn: ai_generate_subject });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function usePersistSubjectFromAI() {
|
||||||
|
const qc = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (payload: { planId: UUID; jsonMateria: any }) => subjects_persist_from_ai(payload),
|
||||||
|
onSuccess: (subject) => {
|
||||||
|
qc.setQueryData(qk.asignatura(subject.id), subject);
|
||||||
|
qc.invalidateQueries({ queryKey: qk.planAsignaturas(subject.plan_estudio_id) });
|
||||||
|
qc.invalidateQueries({ queryKey: qk.planHistorial(subject.plan_estudio_id) });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useCloneSubject() {
|
||||||
|
const qc = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: subjects_clone_from_existing,
|
||||||
|
onSuccess: (subject) => {
|
||||||
|
qc.setQueryData(qk.asignatura(subject.id), subject);
|
||||||
|
qc.invalidateQueries({ queryKey: qk.planAsignaturas(subject.plan_estudio_id) });
|
||||||
|
qc.invalidateQueries({ queryKey: qk.planHistorial(subject.plan_estudio_id) });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useImportSubjectFromFile() {
|
||||||
|
const qc = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: subjects_import_from_file,
|
||||||
|
onSuccess: (subject) => {
|
||||||
|
qc.setQueryData(qk.asignatura(subject.id), subject);
|
||||||
|
qc.invalidateQueries({ queryKey: qk.planAsignaturas(subject.plan_estudio_id) });
|
||||||
|
qc.invalidateQueries({ queryKey: qk.planHistorial(subject.plan_estudio_id) });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useUpdateSubjectFields() {
|
||||||
|
const qc = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (vars: { subjectId: UUID; patch: SubjectsUpdateFieldsPatch }) =>
|
||||||
|
subjects_update_fields(vars.subjectId, vars.patch),
|
||||||
|
onSuccess: (updated) => {
|
||||||
|
qc.setQueryData(qk.asignatura(updated.id), updated);
|
||||||
|
qc.invalidateQueries({ queryKey: qk.planAsignaturas(updated.plan_estudio_id) });
|
||||||
|
qc.invalidateQueries({ queryKey: qk.asignaturaHistorial(updated.id) });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useUpdateSubjectContenido() {
|
||||||
|
const qc = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (vars: { subjectId: UUID; unidades: any[] }) =>
|
||||||
|
subjects_update_contenido(vars.subjectId, vars.unidades),
|
||||||
|
onSuccess: (updated) => {
|
||||||
|
qc.setQueryData(qk.asignatura(updated.id), updated);
|
||||||
|
qc.invalidateQueries({ queryKey: qk.asignaturaHistorial(updated.id) });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useUpdateSubjectBibliografia() {
|
||||||
|
const qc = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (vars: { subjectId: UUID; entries: BibliografiaUpsertInput }) =>
|
||||||
|
subjects_update_bibliografia(vars.subjectId, vars.entries),
|
||||||
|
onSuccess: (_ok, vars) => {
|
||||||
|
qc.invalidateQueries({ queryKey: qk.asignaturaBibliografia(vars.subjectId) });
|
||||||
|
qc.invalidateQueries({ queryKey: qk.asignaturaHistorial(vars.subjectId) });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useGenerateSubjectDocumento() {
|
||||||
|
const qc = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (subjectId: UUID) => subjects_generate_document(subjectId),
|
||||||
|
onSuccess: (_doc, subjectId) => {
|
||||||
|
qc.invalidateQueries({ queryKey: qk.asignaturaDocumento(subjectId) });
|
||||||
|
qc.invalidateQueries({ queryKey: qk.asignaturaHistorial(subjectId) });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
22
src/data/hooks/useTasks.ts
Normal file
22
src/data/hooks/useTasks.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { qk } from "../query/keys";
|
||||||
|
import { tareas_marcar_completada, tareas_mias_list } from "../api/tasks.api";
|
||||||
|
|
||||||
|
export function useMisTareas() {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: qk.tareas(),
|
||||||
|
queryFn: tareas_mias_list,
|
||||||
|
staleTime: 15_000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useMarcarTareaCompletada() {
|
||||||
|
const qc = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: tareas_marcar_completada,
|
||||||
|
onSuccess: () => {
|
||||||
|
qc.invalidateQueries({ queryKey: qk.tareas() });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
23
src/data/index.ts
Normal file
23
src/data/index.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
export * from "./supabase/client";
|
||||||
|
export * from "./supabase/invokeEdge";
|
||||||
|
|
||||||
|
export * from "./query/queryClient";
|
||||||
|
export * from "./query/keys";
|
||||||
|
|
||||||
|
export * from "./types/domain";
|
||||||
|
|
||||||
|
export * from "./api/meta.api";
|
||||||
|
export * from "./api/plans.api";
|
||||||
|
export * from "./api/subjects.api";
|
||||||
|
export * from "./api/files.api";
|
||||||
|
export * from "./api/ai.api";
|
||||||
|
export * from "./api/tasks.api";
|
||||||
|
export * from "./api/notifications.api";
|
||||||
|
|
||||||
|
export * from "./hooks/useAuth";
|
||||||
|
export * from "./hooks/useMeta";
|
||||||
|
export * from "./hooks/usePlans";
|
||||||
|
export * from "./hooks/useSubjects";
|
||||||
|
export * from "./hooks/useAI";
|
||||||
|
export * from "./hooks/useTasks";
|
||||||
|
export * from "./hooks/useNotifications";
|
||||||
31
src/data/query/keys.ts
Normal file
31
src/data/query/keys.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
export const qk = {
|
||||||
|
auth: ["auth"] as const,
|
||||||
|
session: () => ["auth", "session"] as const,
|
||||||
|
meProfile: () => ["auth", "meProfile"] as const,
|
||||||
|
|
||||||
|
facultades: () => ["meta", "facultades"] as const,
|
||||||
|
carreras: (facultadId?: string | null) =>
|
||||||
|
["meta", "carreras", { facultadId: facultadId ?? null }] as const,
|
||||||
|
estructurasPlan: (nivel?: string | null) =>
|
||||||
|
["meta", "estructurasPlan", { nivel: nivel ?? null }] as const,
|
||||||
|
estructurasAsignatura: () => ["meta", "estructurasAsignatura"] as const,
|
||||||
|
estadosPlan: () => ["meta", "estadosPlan"] as const,
|
||||||
|
|
||||||
|
planesList: (filters: unknown) => ["planes", "list", filters] as const,
|
||||||
|
plan: (planId: string) => ["planes", "detail", planId] as const,
|
||||||
|
planLineas: (planId: string) => ["planes", planId, "lineas"] as const,
|
||||||
|
planAsignaturas: (planId: string) => ["planes", planId, "asignaturas"] as const,
|
||||||
|
planHistorial: (planId: string) => ["planes", planId, "historial"] as const,
|
||||||
|
planDocumento: (planId: string) => ["planes", planId, "documento"] as const,
|
||||||
|
|
||||||
|
asignatura: (asignaturaId: string) => ["asignaturas", "detail", asignaturaId] as const,
|
||||||
|
asignaturaBibliografia: (asignaturaId: string) =>
|
||||||
|
["asignaturas", asignaturaId, "bibliografia"] as const,
|
||||||
|
asignaturaHistorial: (asignaturaId: string) =>
|
||||||
|
["asignaturas", asignaturaId, "historial"] as const,
|
||||||
|
asignaturaDocumento: (asignaturaId: string) =>
|
||||||
|
["asignaturas", asignaturaId, "documento"] as const,
|
||||||
|
|
||||||
|
tareas: () => ["tareas", "mias"] as const,
|
||||||
|
notificaciones: () => ["notificaciones", "mias"] as const,
|
||||||
|
};
|
||||||
14
src/data/query/queryClient.ts
Normal file
14
src/data/query/queryClient.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { QueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
export const queryClient = new QueryClient({
|
||||||
|
defaultOptions: {
|
||||||
|
queries: {
|
||||||
|
staleTime: 30_000,
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
retry: (failureCount) => failureCount < 2,
|
||||||
|
},
|
||||||
|
mutations: {
|
||||||
|
retry: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
31
src/data/supabase/client.ts
Normal file
31
src/data/supabase/client.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { createClient, type SupabaseClient } from "@supabase/supabase-js";
|
||||||
|
import type { Database } from "../types/database";
|
||||||
|
import { getEnv } from "./env";
|
||||||
|
|
||||||
|
let _client: SupabaseClient<Database> | null = null;
|
||||||
|
|
||||||
|
export function supabaseBrowser(): SupabaseClient<Database> {
|
||||||
|
if (_client) return _client;
|
||||||
|
|
||||||
|
const url = getEnv(
|
||||||
|
"VITE_SUPABASE_URL",
|
||||||
|
"NEXT_PUBLIC_SUPABASE_URL",
|
||||||
|
"SUPABASE_URL"
|
||||||
|
);
|
||||||
|
|
||||||
|
const anonKey = getEnv(
|
||||||
|
"VITE_SUPABASE_ANON_KEY",
|
||||||
|
"NEXT_PUBLIC_SUPABASE_ANON_KEY",
|
||||||
|
"SUPABASE_ANON_KEY"
|
||||||
|
);
|
||||||
|
|
||||||
|
_client = createClient<Database>(url, anonKey, {
|
||||||
|
auth: {
|
||||||
|
persistSession: true,
|
||||||
|
autoRefreshToken: true,
|
||||||
|
detectSessionInUrl: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return _client;
|
||||||
|
}
|
||||||
17
src/data/supabase/env.ts
Normal file
17
src/data/supabase/env.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
export function getEnv(...keys: string[]): string {
|
||||||
|
for (const key of keys) {
|
||||||
|
const fromProcess =
|
||||||
|
typeof process !== "undefined" ? (process as any).env?.[key] : undefined;
|
||||||
|
|
||||||
|
// Vite / bundlers
|
||||||
|
const fromImportMeta =
|
||||||
|
typeof import.meta !== "undefined" ? (import.meta as any).env?.[key] : undefined;
|
||||||
|
|
||||||
|
const value = fromProcess ?? fromImportMeta;
|
||||||
|
if (typeof value === "string" && value.trim().length > 0) return value.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(
|
||||||
|
`Falta variable de entorno. Probé: ${keys.join(", ")}`
|
||||||
|
);
|
||||||
|
}
|
||||||
47
src/data/supabase/invokeEdge.ts
Normal file
47
src/data/supabase/invokeEdge.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import type { SupabaseClient } from "@supabase/supabase-js";
|
||||||
|
import type { Database } from "../types/database";
|
||||||
|
import { supabaseBrowser } from "./client";
|
||||||
|
|
||||||
|
export type EdgeInvokeOptions = {
|
||||||
|
method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
||||||
|
headers?: Record<string, string>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class EdgeFunctionError extends Error {
|
||||||
|
constructor(
|
||||||
|
message: string,
|
||||||
|
public readonly functionName: string,
|
||||||
|
public readonly status?: number,
|
||||||
|
public readonly details?: unknown
|
||||||
|
) {
|
||||||
|
super(message);
|
||||||
|
this.name = "EdgeFunctionError";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function invokeEdge<TOut>(
|
||||||
|
functionName: string,
|
||||||
|
body?: unknown,
|
||||||
|
opts: EdgeInvokeOptions = {},
|
||||||
|
client?: SupabaseClient<Database>
|
||||||
|
): Promise<TOut> {
|
||||||
|
const supabase = client ?? supabaseBrowser();
|
||||||
|
|
||||||
|
const { data, error } = await supabase.functions.invoke(functionName, {
|
||||||
|
body,
|
||||||
|
method: opts.method ?? "POST",
|
||||||
|
headers: opts.headers,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
const anyErr = error as any;
|
||||||
|
throw new EdgeFunctionError(
|
||||||
|
anyErr.message ?? "Error en Edge Function",
|
||||||
|
functionName,
|
||||||
|
anyErr.status,
|
||||||
|
anyErr
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return data as TOut;
|
||||||
|
}
|
||||||
9
src/data/types/database.ts
Normal file
9
src/data/types/database.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
export type Json =
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| null
|
||||||
|
| { [key: string]: Json }
|
||||||
|
| Json[];
|
||||||
|
|
||||||
|
export type Database = any; // ✅ Reemplaza por tipos generados (supabase gen types typescript)
|
||||||
275
src/data/types/domain.ts
Normal file
275
src/data/types/domain.ts
Normal file
@@ -0,0 +1,275 @@
|
|||||||
|
import type { Json } from "./database";
|
||||||
|
|
||||||
|
export type UUID = string;
|
||||||
|
|
||||||
|
export type TipoEstructuraPlan = "CURRICULAR" | "NO_CURRICULAR";
|
||||||
|
export type NivelPlanEstudio =
|
||||||
|
| "LICENCIATURA"
|
||||||
|
| "MAESTRIA"
|
||||||
|
| "DOCTORADO"
|
||||||
|
| "ESPECIALIDAD"
|
||||||
|
| "DIPLOMADO"
|
||||||
|
| "OTRO";
|
||||||
|
|
||||||
|
export type TipoCiclo = "SEMESTRE" | "CUATRIMESTRE" | "TRIMESTRE" | "OTRO";
|
||||||
|
|
||||||
|
export type TipoOrigen = "MANUAL" | "IA" | "CLONADO_INTERNO" | "TRADICIONAL" | "OTRO";
|
||||||
|
|
||||||
|
export type TipoAsignatura = "OBLIGATORIA" | "OPTATIVA" | "TRONCAL" | "OTRA";
|
||||||
|
|
||||||
|
export type TipoBibliografia = "BASICA" | "COMPLEMENTARIA";
|
||||||
|
export type TipoFuenteBibliografia = "MANUAL" | "BIBLIOTECA";
|
||||||
|
|
||||||
|
export type EstadoTareaRevision = "PENDIENTE" | "COMPLETADA" | "OMITIDA";
|
||||||
|
export type TipoNotificacion = "PLAN_ASIGNADO" | "ESTADO_CAMBIADO" | "TAREA_ASIGNADA" | "COMENTARIO" | "OTRA";
|
||||||
|
|
||||||
|
export type TipoInteraccionIA = "GENERAR" | "MEJORAR_SECCION" | "CHAT" | "OTRA";
|
||||||
|
|
||||||
|
export type ModalidadEducativa = "Escolar" | "No escolarizada" | "Mixta";
|
||||||
|
export type DisenoCurricular = "Rígido" | "Flexible";
|
||||||
|
|
||||||
|
/** Basado en tu schema JSON (va típicamente dentro de planes_estudio.datos) */
|
||||||
|
export type PlanDatosSep = {
|
||||||
|
nivel?: string;
|
||||||
|
nombre?: string;
|
||||||
|
modalidad_educativa?: ModalidadEducativa;
|
||||||
|
|
||||||
|
antecedente_academico?: string;
|
||||||
|
area_de_estudio?: string;
|
||||||
|
clave_del_plan_de_estudios?: string;
|
||||||
|
|
||||||
|
diseno_curricular?: DisenoCurricular;
|
||||||
|
|
||||||
|
total_de_ciclos_del_plan_de_estudios?: string;
|
||||||
|
duracion_del_ciclo_escolar?: string;
|
||||||
|
carga_horaria_a_la_semana?: number;
|
||||||
|
|
||||||
|
fines_de_aprendizaje_o_formacion?: string;
|
||||||
|
perfil_de_egreso?: string;
|
||||||
|
|
||||||
|
programa_de_investigacion?: string | null;
|
||||||
|
curso_propedeutico?: string | null;
|
||||||
|
|
||||||
|
perfil_de_ingreso?: string;
|
||||||
|
|
||||||
|
administracion_y_operatividad_del_plan_de_estudios?: string | null;
|
||||||
|
sustento_teorico_del_modelo_curricular?: string | null;
|
||||||
|
justificacion_de_la_propuesta_curricular?: string | null;
|
||||||
|
propuesta_de_evaluacion_periodica_del_plan_de_estudios?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Paged<T> = { data: T[]; count: number | null };
|
||||||
|
|
||||||
|
export type Facultad = {
|
||||||
|
id: UUID;
|
||||||
|
nombre: string;
|
||||||
|
nombre_corto: string | null;
|
||||||
|
color: string | null;
|
||||||
|
icono: string | null;
|
||||||
|
creado_en: string;
|
||||||
|
actualizado_en: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Carrera = {
|
||||||
|
id: UUID;
|
||||||
|
facultad_id: UUID;
|
||||||
|
nombre: string;
|
||||||
|
nombre_corto: string | null;
|
||||||
|
clave_sep: string | null;
|
||||||
|
activa: boolean;
|
||||||
|
creado_en: string;
|
||||||
|
actualizado_en: string;
|
||||||
|
|
||||||
|
facultades?: Facultad | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type EstructuraPlan = {
|
||||||
|
id: UUID;
|
||||||
|
nombre: string;
|
||||||
|
tipo: TipoEstructuraPlan;
|
||||||
|
version: string | null;
|
||||||
|
definicion: Json;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type EstructuraAsignatura = {
|
||||||
|
id: UUID;
|
||||||
|
nombre: string;
|
||||||
|
version: string | null;
|
||||||
|
definicion: Json;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type EstadoPlan = {
|
||||||
|
id: UUID;
|
||||||
|
clave: string;
|
||||||
|
etiqueta: string;
|
||||||
|
orden: number;
|
||||||
|
es_final: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PlanEstudio = {
|
||||||
|
id: UUID;
|
||||||
|
carrera_id: UUID;
|
||||||
|
estructura_id: UUID;
|
||||||
|
|
||||||
|
nombre: string;
|
||||||
|
nivel: NivelPlanEstudio;
|
||||||
|
tipo_ciclo: TipoCiclo;
|
||||||
|
numero_ciclos: number;
|
||||||
|
|
||||||
|
datos: Json;
|
||||||
|
|
||||||
|
estado_actual_id: UUID | null;
|
||||||
|
activo: boolean;
|
||||||
|
|
||||||
|
tipo_origen: TipoOrigen | null;
|
||||||
|
meta_origen: Json;
|
||||||
|
|
||||||
|
creado_por: UUID | null;
|
||||||
|
actualizado_por: UUID | null;
|
||||||
|
|
||||||
|
creado_en: string;
|
||||||
|
actualizado_en: string;
|
||||||
|
|
||||||
|
carreras?: Carrera | null;
|
||||||
|
estructuras_plan?: EstructuraPlan | null;
|
||||||
|
estados_plan?: EstadoPlan | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type LineaPlan = {
|
||||||
|
id: UUID;
|
||||||
|
plan_estudio_id: UUID;
|
||||||
|
nombre: string;
|
||||||
|
orden: number;
|
||||||
|
area: string | null;
|
||||||
|
creado_en: string;
|
||||||
|
actualizado_en: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Asignatura = {
|
||||||
|
id: UUID;
|
||||||
|
plan_estudio_id: UUID;
|
||||||
|
estructura_id: UUID | null;
|
||||||
|
|
||||||
|
facultad_propietaria_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;
|
||||||
|
orden_celda: number | null;
|
||||||
|
|
||||||
|
datos: Json;
|
||||||
|
contenido_tematico: Json;
|
||||||
|
|
||||||
|
tipo_origen: TipoOrigen | null;
|
||||||
|
meta_origen: Json;
|
||||||
|
|
||||||
|
creado_por: UUID | null;
|
||||||
|
actualizado_por: UUID | null;
|
||||||
|
|
||||||
|
creado_en: string;
|
||||||
|
actualizado_en: string;
|
||||||
|
|
||||||
|
planes_estudio?: PlanEstudio | null;
|
||||||
|
estructuras_asignatura?: EstructuraAsignatura | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BibliografiaAsignatura = {
|
||||||
|
id: UUID;
|
||||||
|
asignatura_id: UUID;
|
||||||
|
tipo: TipoBibliografia;
|
||||||
|
cita: string;
|
||||||
|
tipo_fuente: TipoFuenteBibliografia;
|
||||||
|
biblioteca_item_id: string | null;
|
||||||
|
|
||||||
|
creado_por: UUID | null;
|
||||||
|
creado_en: string;
|
||||||
|
actualizado_en: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CambioPlan = {
|
||||||
|
id: UUID;
|
||||||
|
plan_estudio_id: UUID;
|
||||||
|
cambiado_por: UUID | null;
|
||||||
|
cambiado_en: string;
|
||||||
|
tipo: "ACTUALIZACION_CAMPO" | "ACTUALIZACION_MAPA" | "OTRO";
|
||||||
|
campo: string | null;
|
||||||
|
valor_anterior: Json | null;
|
||||||
|
valor_nuevo: Json | null;
|
||||||
|
interaccion_ia_id: UUID | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CambioAsignatura = {
|
||||||
|
id: UUID;
|
||||||
|
asignatura_id: UUID;
|
||||||
|
cambiado_por: UUID | null;
|
||||||
|
cambiado_en: string;
|
||||||
|
tipo: "ACTUALIZACION_CAMPO" | "ACTUALIZACION_MAPA" | "OTRO";
|
||||||
|
campo: string | null;
|
||||||
|
valor_anterior: Json | null;
|
||||||
|
valor_nuevo: Json | null;
|
||||||
|
fuente: "HUMANO" | "IA" | null;
|
||||||
|
interaccion_ia_id: UUID | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type InteraccionIA = {
|
||||||
|
id: UUID;
|
||||||
|
usuario_id: UUID | null;
|
||||||
|
plan_estudio_id: UUID | null;
|
||||||
|
asignatura_id: UUID | null;
|
||||||
|
|
||||||
|
tipo: TipoInteraccionIA;
|
||||||
|
modelo: string | null;
|
||||||
|
temperatura: number | null;
|
||||||
|
|
||||||
|
prompt: Json;
|
||||||
|
respuesta: Json;
|
||||||
|
|
||||||
|
aceptada: boolean;
|
||||||
|
|
||||||
|
conversacion_id: string | null;
|
||||||
|
ids_archivos: Json;
|
||||||
|
ids_vector_store: Json;
|
||||||
|
|
||||||
|
creado_en: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TareaRevision = {
|
||||||
|
id: UUID;
|
||||||
|
plan_estudio_id: UUID;
|
||||||
|
asignado_a: UUID;
|
||||||
|
rol_id: UUID | null;
|
||||||
|
estado_id: UUID | null;
|
||||||
|
estatus: EstadoTareaRevision;
|
||||||
|
fecha_limite: string | null;
|
||||||
|
creado_en: string;
|
||||||
|
completado_en: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Notificacion = {
|
||||||
|
id: UUID;
|
||||||
|
usuario_id: UUID;
|
||||||
|
tipo: TipoNotificacion;
|
||||||
|
payload: Json;
|
||||||
|
leida: boolean;
|
||||||
|
creado_en: string;
|
||||||
|
leida_en: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Archivo = {
|
||||||
|
id: UUID;
|
||||||
|
ruta_storage: string;
|
||||||
|
nombre: string;
|
||||||
|
mime_type: string | null;
|
||||||
|
bytes: number | null;
|
||||||
|
subido_por: UUID | null;
|
||||||
|
subido_en: string;
|
||||||
|
temporal: boolean;
|
||||||
|
openai_file_id: string | null;
|
||||||
|
notas: string | null;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user