tipado desde supabase, primer listado de planes, ajustes en src/data

This commit is contained in:
2026-01-09 15:51:36 -06:00
parent 8d6b7c4ba9
commit 5a7672677d
26 changed files with 7812 additions and 1577 deletions

8924
bun.lock

File diff suppressed because it is too large Load Diff

View File

@@ -124,6 +124,14 @@ export default [
}, },
}, },
// 5. PRETTIER AL FINAL // 5. OVERRIDE: desactivar reglas para tipos generados por supabase
{
files: ['src/types/supabase.ts'],
rules: {
'@typescript-eslint/naming-convention': 'off',
},
},
// 6. PRETTIER AL FINAL
eslintConfigPrettier, eslintConfigPrettier,
] ]

View File

@@ -70,6 +70,7 @@
"jsdom": "^27.0.0", "jsdom": "^27.0.0",
"prettier": "^3.5.3", "prettier": "^3.5.3",
"prettier-plugin-tailwindcss": "^0.7.2", "prettier-plugin-tailwindcss": "^0.7.2",
"supabase": "^2.72.2",
"typescript": "^5.7.2", "typescript": "^5.7.2",
"vite": "^7.1.7", "vite": "^7.1.7",
"vitest": "^3.0.5", "vitest": "^3.0.5",

View File

@@ -67,7 +67,7 @@ export function PasoConfiguracionPanel({
}, },
})) }))
} }
className="min-h-[100px]" className="min-h-25"
/> />
</div> </div>
<div className="grid gap-1"> <div className="grid gap-1">
@@ -213,7 +213,7 @@ export function PasoConfiguracionPanel({
</div> </div>
</div> </div>
<div className="grid max-h-[300px] gap-2 overflow-y-auto"> <div className="grid max-h-75 gap-2 overflow-y-auto">
{MATERIAS_MOCK.map((m) => ( {MATERIAS_MOCK.map((m) => (
<div <div
key={m.id} key={m.id}

View File

@@ -31,7 +31,7 @@ export function StepWithTooltip({
{title} {title}
</span> </span>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent className="max-w-[200px] text-xs"> <TooltipContent className="max-w-50 text-xs">
<p>{desc}</p> <p>{desc}</p>
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>

View File

@@ -32,7 +32,7 @@ export function StepWithTooltip({
{title} {title}
</span> </span>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent className="max-w-[200px] text-xs"> <TooltipContent className="max-w-50 text-xs">
<p>{desc}</p> <p>{desc}</p>
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>

View File

@@ -1,52 +1,54 @@
import * as React from "react" import { Slot } from '@radix-ui/react-slot'
import { Slot } from "@radix-ui/react-slot" import { cva } from 'class-variance-authority'
import { cva, type VariantProps } from "class-variance-authority" import * as React from 'react'
import { cn } from "@/lib/utils" import type { VariantProps } from 'class-variance-authority'
import { cn } from '@/lib/utils'
const buttonVariants = cva( const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
{ {
variants: { variants: {
variant: { variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90", default: 'bg-primary text-primary-foreground hover:bg-primary/90',
destructive: destructive:
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", 'bg-destructive hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60 text-white',
outline: outline:
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", 'bg-background hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 border shadow-xs',
secondary: secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80", 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
ghost: ghost:
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", 'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
link: "text-primary underline-offset-4 hover:underline", link: 'text-primary underline-offset-4 hover:underline',
}, },
size: { size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3", default: 'h-9 px-4 py-2 has-[>svg]:px-3',
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", sm: 'h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5',
lg: "h-10 rounded-md px-6 has-[>svg]:px-4", lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
icon: "size-9", icon: 'size-9',
"icon-sm": "size-8", 'icon-sm': 'size-8',
"icon-lg": "size-10", 'icon-lg': 'size-10',
}, },
}, },
defaultVariants: { defaultVariants: {
variant: "default", variant: 'default',
size: "default", size: 'default',
},
}, },
}
) )
function Button({ function Button({
className, className,
variant = "default", variant = 'default',
size = "default", size = 'default',
asChild = false, asChild = false,
...props ...props
}: React.ComponentProps<"button"> & }: React.ComponentProps<'button'> &
VariantProps<typeof buttonVariants> & { VariantProps<typeof buttonVariants> & {
asChild?: boolean asChild?: boolean
}) { }) {
const Comp = asChild ? Slot : "button" const Comp = asChild ? Slot : 'button'
return ( return (
<Comp <Comp

View File

@@ -1,6 +1,8 @@
import { supabaseBrowser } from "../supabase/client"; import { supabaseBrowser } from "../supabase/client";
import { invokeEdge } from "../supabase/invokeEdge"; import { invokeEdge } from "../supabase/invokeEdge";
import { buildRange, throwIfError, requireData } from "./_helpers";
import { buildRange, requireData, throwIfError } from "./_helpers";
import type { import type {
Asignatura, Asignatura,
CambioPlan, CambioPlan,
@@ -18,6 +20,7 @@ const EDGE = {
ai_generate_plan: "ai_generate_plan", ai_generate_plan: "ai_generate_plan",
plans_persist_from_ai: "plans_persist_from_ai", plans_persist_from_ai: "plans_persist_from_ai",
plans_clone_from_existing: "plans_clone_from_existing", plans_clone_from_existing: "plans_clone_from_existing",
plans_import_from_files: "plans_import_from_files", plans_import_from_files: "plans_import_from_files",
plans_update_fields: "plans_update_fields", plans_update_fields: "plans_update_fields",
@@ -39,23 +42,33 @@ export type PlanListFilters = {
offset?: number; offset?: number;
}; };
export async function plans_list(filters: PlanListFilters = {}): Promise<Paged<PlanEstudio>> { export async function plans_list(
filters: PlanListFilters = {},
): Promise<Paged<PlanEstudio>> {
const supabase = supabaseBrowser(); const supabase = supabaseBrowser();
// 1. Construimos la query.
// TypeScript validará que "planes_estudio" existe en Database
let q = supabase let q = supabase
.from("planes_estudio") .from("planes_estudio")
.select( .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)), carreras (
estructuras_plan(id,nombre,tipo,version,definicion), *,
estados_plan(id,clave,etiqueta,orden,es_final) facultades (*)
),
estructuras_plan (*),
estados_plan (*)
`, `,
{ count: "exact" } { count: "exact" },
) )
.order("actualizado_en", { ascending: false }); .order("actualizado_en", { ascending: false });
if (filters.search?.trim()) q = q.ilike("nombre", `%${filters.search.trim()}%`); // 2. Aplicamos filtros dinámicos
if (filters.search?.trim()) {
q = q.ilike("nombre", `%${filters.search.trim()}%`);
}
if (filters.carreraId) q = q.eq("carrera_id", filters.carreraId); if (filters.carreraId) q = q.eq("carrera_id", filters.carreraId);
if (filters.estadoId) q = q.eq("estado_actual_id", filters.estadoId); if (filters.estadoId) q = q.eq("estado_actual_id", filters.estadoId);
if (typeof filters.activo === "boolean") q = q.eq("activo", filters.activo); if (typeof filters.activo === "boolean") q = q.eq("activo", filters.activo);
@@ -63,13 +76,19 @@ export async function plans_list(filters: PlanListFilters = {}): Promise<Paged<P
// filtro por FK “hacia arriba” (PostgREST soporta filtros en recursos embebidos) // filtro por FK “hacia arriba” (PostgREST soporta filtros en recursos embebidos)
if (filters.facultadId) q = q.eq("carreras.facultad_id", filters.facultadId); if (filters.facultadId) q = q.eq("carreras.facultad_id", filters.facultadId);
// 3. Paginación
const { from, to } = buildRange(filters.limit, filters.offset); const { from, to } = buildRange(filters.limit, filters.offset);
if (typeof from === "number" && typeof to === "number") q = q.range(from, to); if (from !== undefined && to !== undefined) q = q.range(from, to);
const { data, error, count } = await q; const { data, error, count } = await q;
throwIfError(error); throwIfError(error);
return { data: data ?? [], count: count ?? null }; return {
// 1. Si data es null, usa [].
// 2. Luego dile a TS que el resultado es tu Array tipado.
data: (data ?? []) as unknown as Array<PlanEstudio>,
count: count ?? 0,
};
} }
export async function plans_get(planId: UUID): Promise<PlanEstudio> { export async function plans_get(planId: UUID): Promise<PlanEstudio> {
@@ -79,11 +98,11 @@ export async function plans_get(planId: UUID): Promise<PlanEstudio> {
.from("planes_estudio") .from("planes_estudio")
.select( .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)), carreras (*, facultades(*)),
estructuras_plan(id,nombre,tipo,version,definicion), estructuras_plan (*),
estados_plan(id,clave,etiqueta,orden,es_final) estados_plan (*)
` `,
) )
.eq("id", planId) .eq("id", planId)
.single(); .single();
@@ -92,7 +111,9 @@ export async function plans_get(planId: UUID): Promise<PlanEstudio> {
return requireData(data, "Plan no encontrado."); return requireData(data, "Plan no encontrado.");
} }
export async function plan_lineas_list(planId: UUID): Promise<LineaPlan[]> { export async function plan_lineas_list(
planId: UUID,
): Promise<Array<LineaPlan>> {
const supabase = supabaseBrowser(); const supabase = supabaseBrowser();
const { data, error } = await supabase const { data, error } = await supabase
.from("lineas_plan") .from("lineas_plan")
@@ -104,12 +125,14 @@ export async function plan_lineas_list(planId: UUID): Promise<LineaPlan[]> {
return data ?? []; return data ?? [];
} }
export async function plan_asignaturas_list(planId: UUID): Promise<Asignatura[]> { export async function plan_asignaturas_list(
planId: UUID,
): Promise<Array<Asignatura>> {
const supabase = supabaseBrowser(); const supabase = supabaseBrowser();
const { data, error } = await supabase const { data, error } = await supabase
.from("asignaturas") .from("asignaturas")
.select( .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" "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) .eq("plan_estudio_id", planId)
.order("numero_ciclo", { ascending: true, nullsFirst: false }) .order("numero_ciclo", { ascending: true, nullsFirst: false })
@@ -120,11 +143,13 @@ export async function plan_asignaturas_list(planId: UUID): Promise<Asignatura[]>
return data ?? []; return data ?? [];
} }
export async function plans_history(planId: UUID): Promise<CambioPlan[]> { export async function plans_history(planId: UUID): Promise<Array<CambioPlan>> {
const supabase = supabaseBrowser(); const supabase = supabaseBrowser();
const { data, error } = await supabase const { data, error } = await supabase
.from("cambios_plan") .from("cambios_plan")
.select("id,plan_estudio_id,cambiado_por,cambiado_en,tipo,campo,valor_anterior,valor_nuevo,interaccion_ia_id") .select(
"id,plan_estudio_id,cambiado_por,cambiado_en,tipo,campo,valor_anterior,valor_nuevo,interaccion_ia_id",
)
.eq("plan_estudio_id", planId) .eq("plan_estudio_id", planId)
.order("cambiado_en", { ascending: false }); .order("cambiado_en", { ascending: false });
@@ -143,7 +168,9 @@ export type PlansCreateManualInput = {
datos?: Partial<PlanDatosSep> & Record<string, any>; datos?: Partial<PlanDatosSep> & Record<string, any>;
}; };
export async function plans_create_manual(input: PlansCreateManualInput): Promise<PlanEstudio> { export async function plans_create_manual(
input: PlansCreateManualInput,
): Promise<PlanEstudio> {
return invokeEdge<PlanEstudio>(EDGE.plans_create_manual, input); return invokeEdge<PlanEstudio>(EDGE.plans_create_manual, input);
} }
@@ -161,23 +188,31 @@ export type AIGeneratePlanInput = {
descripcionEnfoque: string; descripcionEnfoque: string;
poblacionObjetivo?: string; poblacionObjetivo?: string;
notasAdicionales?: string; notasAdicionales?: string;
archivosReferencia?: UUID[]; archivosReferencia?: Array<UUID>;
repositoriosIds?: UUID[]; repositoriosIds?: Array<UUID>;
usarMCP?: boolean; usarMCP?: boolean;
}; };
}; };
export async function ai_generate_plan(input: AIGeneratePlanInput): Promise<any> { export async function ai_generate_plan(
input: AIGeneratePlanInput,
): Promise<any> {
return invokeEdge<any>(EDGE.ai_generate_plan, input); return invokeEdge<any>(EDGE.ai_generate_plan, input);
} }
export async function plans_persist_from_ai(payload: { jsonPlan: any }): Promise<PlanEstudio> { export async function plans_persist_from_ai(
payload: { jsonPlan: any },
): Promise<PlanEstudio> {
return invokeEdge<PlanEstudio>(EDGE.plans_persist_from_ai, payload); return invokeEdge<PlanEstudio>(EDGE.plans_persist_from_ai, payload);
} }
export async function plans_clone_from_existing(payload: { export async function plans_clone_from_existing(payload: {
planOrigenId: UUID; planOrigenId: UUID;
overrides: Partial<Pick<PlanEstudio, "nombre" | "nivel" | "tipo_ciclo" | "numero_ciclos">> & { overrides:
& Partial<
Pick<PlanEstudio, "nombre" | "nivel" | "tipo_ciclo" | "numero_ciclos">
>
& {
carrera_id?: UUID; carrera_id?: UUID;
estructura_id?: UUID; estructura_id?: UUID;
datos?: Partial<PlanDatosSep> & Record<string, any>; datos?: Partial<PlanDatosSep> & Record<string, any>;
@@ -211,7 +246,10 @@ export type PlansUpdateFieldsPatch = {
datos?: Partial<PlanDatosSep> & Record<string, any>; datos?: Partial<PlanDatosSep> & Record<string, any>;
}; };
export async function plans_update_fields(planId: UUID, patch: PlansUpdateFieldsPatch): Promise<PlanEstudio> { export async function plans_update_fields(
planId: UUID,
patch: PlansUpdateFieldsPatch,
): Promise<PlanEstudio> {
return invokeEdge<PlanEstudio>(EDGE.plans_update_fields, { planId, patch }); return invokeEdge<PlanEstudio>(EDGE.plans_update_fields, { planId, patch });
} }
@@ -228,10 +266,13 @@ export type PlanMapOperation =
op: "REORDER_CELDA"; op: "REORDER_CELDA";
linea_plan_id: UUID; linea_plan_id: UUID;
numero_ciclo: number; numero_ciclo: number;
asignaturaIdsOrdenados: UUID[]; asignaturaIdsOrdenados: Array<UUID>;
}; };
export async function plans_update_map(planId: UUID, ops: PlanMapOperation[]): Promise<{ ok: true }> { export async function plans_update_map(
planId: UUID,
ops: Array<PlanMapOperation>,
): Promise<{ ok: true }> {
return invokeEdge<{ ok: true }>(EDGE.plans_update_map, { planId, ops }); return invokeEdge<{ ok: true }>(EDGE.plans_update_map, { planId, ops });
} }
@@ -251,10 +292,16 @@ export type DocumentoResult = {
nombre?: string; nombre?: string;
}; };
export async function plans_generate_document(planId: UUID): Promise<DocumentoResult> { export async function plans_generate_document(
planId: UUID,
): Promise<DocumentoResult> {
return invokeEdge<DocumentoResult>(EDGE.plans_generate_document, { planId }); return invokeEdge<DocumentoResult>(EDGE.plans_generate_document, { planId });
} }
export async function plans_get_document(planId: UUID): Promise<DocumentoResult | null> { export async function plans_get_document(
return invokeEdge<DocumentoResult | null>(EDGE.plans_get_document, { planId }); planId: UUID,
): Promise<DocumentoResult | null> {
return invokeEdge<DocumentoResult | null>(EDGE.plans_get_document, {
planId,
});
} }

View File

@@ -1,4 +1,5 @@
import { useMutation } from "@tanstack/react-query"; import { useMutation } from "@tanstack/react-query";
import { import {
ai_plan_chat, ai_plan_chat,
ai_plan_improve, ai_plan_improve,

View File

@@ -1,7 +1,10 @@
import { keepPreviousData, useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import {
import { qk } from "../query/keys"; keepPreviousData,
import type { PlanEstudio, UUID } from "../types/domain"; useMutation,
import type { PlanListFilters, PlanMapOperation, PlansCreateManualInput, PlansUpdateFieldsPatch } from "../api/plans.api"; useQuery,
useQueryClient,
} from "@tanstack/react-query";
import { import {
ai_generate_plan, ai_generate_plan,
plan_asignaturas_list, plan_asignaturas_list,
@@ -19,13 +22,30 @@ import {
plans_update_fields, plans_update_fields,
plans_update_map, plans_update_map,
} from "../api/plans.api"; } from "../api/plans.api";
import { qk } from "../query/keys";
import type {
PlanListFilters,
PlanMapOperation,
PlansCreateManualInput,
PlansUpdateFieldsPatch,
} from "../api/plans.api";
import type { UUID } from "../types/domain";
export function usePlanes(filters: PlanListFilters) { export function usePlanes(filters: PlanListFilters) {
// 🧠 Tip: memoiza "filters" (useMemo) para que queryKey sea estable. // 🧠 Tip: memoiza "filters" (useMemo) para que queryKey sea estable.
return useQuery({ return useQuery({
// Usamos la factory de keys para consistencia
queryKey: qk.planesList(filters), queryKey: qk.planesList(filters),
// La función fetch
queryFn: () => plans_list(filters), queryFn: () => plans_list(filters),
// UX: Mantiene los datos viejos mientras carga la paginación nueva
placeholderData: keepPreviousData, placeholderData: keepPreviousData,
// Opcional: Tiempo que la data se considera fresca
staleTime: 1000 * 60 * 5, // 5 minutos
}); });
} }
@@ -47,7 +67,9 @@ export function usePlanLineas(planId: UUID | null | undefined) {
export function usePlanAsignaturas(planId: UUID | null | undefined) { export function usePlanAsignaturas(planId: UUID | null | undefined) {
return useQuery({ return useQuery({
queryKey: planId ? qk.planAsignaturas(planId) : ["planes", "asignaturas", null], queryKey: planId
? qk.planAsignaturas(planId)
: ["planes", "asignaturas", null],
queryFn: () => plan_asignaturas_list(planId as UUID), queryFn: () => plan_asignaturas_list(planId as UUID),
enabled: Boolean(planId), enabled: Boolean(planId),
}); });
@@ -144,7 +166,8 @@ export function useUpdatePlanMapa() {
const qc = useQueryClient(); const qc = useQueryClient();
return useMutation({ return useMutation({
mutationFn: (vars: { planId: UUID; ops: PlanMapOperation[] }) => plans_update_map(vars.planId, vars.ops), mutationFn: (vars: { planId: UUID; ops: Array<PlanMapOperation> }) =>
plans_update_map(vars.planId, vars.ops),
// ✅ Optimista (rápida) para el caso MOVE_ASIGNATURA // ✅ Optimista (rápida) para el caso MOVE_ASIGNATURA
onMutate: async (vars) => { onMutate: async (vars) => {
@@ -152,9 +175,7 @@ export function useUpdatePlanMapa() {
const prev = qc.getQueryData<any>(qk.planAsignaturas(vars.planId)); const prev = qc.getQueryData<any>(qk.planAsignaturas(vars.planId));
// solo optimizamos MOVEs simples // solo optimizamos MOVEs simples
const moves = vars.ops.filter((x) => x.op === "MOVE_ASIGNATURA") as Array< const moves = vars.ops.filter((x) => x.op === "MOVE_ASIGNATURA");
Extract<PlanMapOperation, { op: "MOVE_ASIGNATURA" }>
>;
if (prev && Array.isArray(prev) && moves.length) { if (prev && Array.isArray(prev) && moves.length) {
const next = prev.map((a: any) => { const next = prev.map((a: any) => {

View File

@@ -1,14 +0,0 @@
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,
},
},
});

View File

@@ -1,7 +1,20 @@
import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
export function getContext() { export function getContext() {
const queryClient = new QueryClient() const queryClient = new QueryClient(
{
defaultOptions: {
queries: {
staleTime: 30_000,
refetchOnWindowFocus: false,
retry: (failureCount) => failureCount < 2,
},
mutations: {
retry: 0,
},
},
}
)
return { return {
queryClient, queryClient,
} }

View File

@@ -1,7 +1,10 @@
import { createClient, type SupabaseClient } from "@supabase/supabase-js"; import { createClient } from "@supabase/supabase-js";
import type { Database } from "../types/database";
import { getEnv } from "./env"; import { getEnv } from "./env";
import type { SupabaseClient } from "@supabase/supabase-js";
import type { Database } from "src/types/supabase.js";
let _client: SupabaseClient<Database> | null = null; let _client: SupabaseClient<Database> | null = null;
export function supabaseBrowser(): SupabaseClient<Database> { export function supabaseBrowser(): SupabaseClient<Database> {
@@ -10,13 +13,13 @@ export function supabaseBrowser(): SupabaseClient<Database> {
const url = getEnv( const url = getEnv(
"VITE_SUPABASE_URL", "VITE_SUPABASE_URL",
"NEXT_PUBLIC_SUPABASE_URL", "NEXT_PUBLIC_SUPABASE_URL",
"SUPABASE_URL" "SUPABASE_URL",
); );
const anonKey = getEnv( const anonKey = getEnv(
"VITE_SUPABASE_ANON_KEY", "VITE_SUPABASE_ANON_KEY",
"NEXT_PUBLIC_SUPABASE_ANON_KEY", "NEXT_PUBLIC_SUPABASE_ANON_KEY",
"SUPABASE_ANON_KEY" "SUPABASE_ANON_KEY",
); );
_client = createClient<Database>(url, anonKey, { _client = createClient<Database>(url, anonKey, {

View File

@@ -1,27 +1,26 @@
import type { Json } from "./database"; import type { Enums, Tables } from "../../types/supabase";
import type { Json } from "src/types/supabase";
export type UUID = string; export type UUID = string;
export type TipoEstructuraPlan = "CURRICULAR" | "NO_CURRICULAR"; export type TipoEstructuraPlan = Enums<"tipo_estructura_plan">;
export type NivelPlanEstudio = export type NivelPlanEstudio = Enums<"nivel_plan_estudio">;
| "LICENCIATURA" export type TipoCiclo = Enums<"tipo_ciclo">;
| "MAESTRIA"
| "DOCTORADO"
| "ESPECIALIDAD"
| "DIPLOMADO"
| "OTRO";
export type TipoCiclo = "SEMESTRE" | "CUATRIMESTRE" | "TRIMESTRE" | "OTRO"; export type TipoOrigen = Enums<"tipo_origen">;
export type TipoOrigen = "MANUAL" | "IA" | "CLONADO_INTERNO" | "TRADICIONAL" | "OTRO"; export type TipoAsignatura = Enums<"tipo_asignatura">;
export type TipoAsignatura = "OBLIGATORIA" | "OPTATIVA" | "TRONCAL" | "OTRA";
export type TipoBibliografia = "BASICA" | "COMPLEMENTARIA"; export type TipoBibliografia = "BASICA" | "COMPLEMENTARIA";
export type TipoFuenteBibliografia = "MANUAL" | "BIBLIOTECA"; export type TipoFuenteBibliografia = "MANUAL" | "BIBLIOTECA";
export type EstadoTareaRevision = "PENDIENTE" | "COMPLETADA" | "OMITIDA"; export type EstadoTareaRevision = "PENDIENTE" | "COMPLETADA" | "OMITIDA";
export type TipoNotificacion = "PLAN_ASIGNADO" | "ESTADO_CAMBIADO" | "TAREA_ASIGNADA" | "COMENTARIO" | "OTRA"; export type TipoNotificacion =
| "PLAN_ASIGNADO"
| "ESTADO_CAMBIADO"
| "TAREA_ASIGNADA"
| "COMENTARIO"
| "OTRA";
export type TipoInteraccionIA = "GENERAR" | "MEJORAR_SECCION" | "CHAT" | "OTRA"; export type TipoInteraccionIA = "GENERAR" | "MEJORAR_SECCION" | "CHAT" | "OTRA";
@@ -58,38 +57,12 @@ export type PlanDatosSep = {
propuesta_de_evaluacion_periodica_del_plan_de_estudios?: string | null; propuesta_de_evaluacion_periodica_del_plan_de_estudios?: string | null;
}; };
export type Paged<T> = { data: T[]; count: number | null }; export type Paged<T> = { data: Array<T>; count: number | null };
export type Facultad = { export type FacultadRow = Tables<"facultades">;
id: UUID; export type CarreraRow = Tables<"carreras">;
nombre: string;
nombre_corto: string | null;
color: string | null;
icono: string | null;
creado_en: string;
actualizado_en: string;
};
export type Carrera = { export type EstructuraPlanRow = Tables<"estructuras_plan">;
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 = { export type EstructuraAsignatura = {
id: UUID; id: UUID;
@@ -98,41 +71,13 @@ export type EstructuraAsignatura = {
definicion: Json; definicion: Json;
}; };
export type EstadoPlan = { export type EstadoPlanRow = Tables<"estados_plan">;
id: UUID; export type PlanEstudioRow = Tables<"planes_estudio">;
clave: string;
etiqueta: string;
orden: number;
es_final: boolean;
};
export type PlanEstudio = { export type PlanEstudio = PlanEstudioRow & {
id: UUID; carreras: (CarreraRow & { facultades: FacultadRow | null }) | null;
carrera_id: UUID; estructuras_plan: EstructuraPlanRow | null;
estructura_id: UUID; estados_plan: EstadoPlanRow | null;
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 = { export type LineaPlan = {

View File

@@ -2,10 +2,11 @@ import { RouterProvider, createRouter } from '@tanstack/react-router'
import { StrictMode } from 'react' import { StrictMode } from 'react'
import ReactDOM from 'react-dom/client' import ReactDOM from 'react-dom/client'
import * as TanStackQueryProvider from './integrations/tanstack-query/root-provider.tsx'
import reportWebVitals from './reportWebVitals.ts' import reportWebVitals from './reportWebVitals.ts'
import { routeTree } from './routeTree.gen' import { routeTree } from './routeTree.gen'
import * as TanStackQueryProvider from '@/data/query/queryClient.tsx'
import './styles.css' import './styles.css'
// Create a new router instance // Create a new router instance

View File

@@ -12,6 +12,7 @@ import { Route as rootRouteImport } from './routes/__root'
import { Route as LoginRouteImport } from './routes/login' import { Route as LoginRouteImport } from './routes/login'
import { Route as DashboardRouteImport } from './routes/dashboard' import { Route as DashboardRouteImport } from './routes/dashboard'
import { Route as IndexRouteImport } from './routes/index' import { Route as IndexRouteImport } from './routes/index'
import { Route as PlanesPlanesListRouteRouteImport } from './routes/planes/PlanesListRoute'
import { Route as DemoTanstackQueryRouteImport } from './routes/demo/tanstack-query' import { Route as DemoTanstackQueryRouteImport } from './routes/demo/tanstack-query'
import { Route as PlanesListaRouteRouteImport } from './routes/planes/_lista/route' import { Route as PlanesListaRouteRouteImport } from './routes/planes/_lista/route'
import { Route as PlanesListaNuevoRouteImport } from './routes/planes/_lista/nuevo' import { Route as PlanesListaNuevoRouteImport } from './routes/planes/_lista/nuevo'
@@ -44,6 +45,11 @@ const IndexRoute = IndexRouteImport.update({
path: '/', path: '/',
getParentRoute: () => rootRouteImport, getParentRoute: () => rootRouteImport,
} as any) } as any)
const PlanesPlanesListRouteRoute = PlanesPlanesListRouteRouteImport.update({
id: '/planes/PlanesListRoute',
path: '/planes/PlanesListRoute',
getParentRoute: () => rootRouteImport,
} as any)
const DemoTanstackQueryRoute = DemoTanstackQueryRouteImport.update({ const DemoTanstackQueryRoute = DemoTanstackQueryRouteImport.update({
id: '/demo/tanstack-query', id: '/demo/tanstack-query',
path: '/demo/tanstack-query', path: '/demo/tanstack-query',
@@ -142,6 +148,7 @@ export interface FileRoutesByFullPath {
'/login': typeof LoginRoute '/login': typeof LoginRoute
'/planes': typeof PlanesListaRouteRouteWithChildren '/planes': typeof PlanesListaRouteRouteWithChildren
'/demo/tanstack-query': typeof DemoTanstackQueryRoute '/demo/tanstack-query': typeof DemoTanstackQueryRoute
'/planes/PlanesListRoute': typeof PlanesPlanesListRouteRoute
'/planes/$planId': typeof PlanesPlanIdDetalleRouteRouteWithChildren '/planes/$planId': typeof PlanesPlanIdDetalleRouteRouteWithChildren
'/planes/$planId/asignaturas': typeof PlanesPlanIdAsignaturasListaRouteRouteWithChildren '/planes/$planId/asignaturas': typeof PlanesPlanIdAsignaturasListaRouteRouteWithChildren
'/planes/nuevo': typeof PlanesListaNuevoRoute '/planes/nuevo': typeof PlanesListaNuevoRoute
@@ -162,6 +169,7 @@ export interface FileRoutesByTo {
'/login': typeof LoginRoute '/login': typeof LoginRoute
'/planes': typeof PlanesListaRouteRouteWithChildren '/planes': typeof PlanesListaRouteRouteWithChildren
'/demo/tanstack-query': typeof DemoTanstackQueryRoute '/demo/tanstack-query': typeof DemoTanstackQueryRoute
'/planes/PlanesListRoute': typeof PlanesPlanesListRouteRoute
'/planes/$planId': typeof PlanesPlanIdDetalleRouteRouteWithChildren '/planes/$planId': typeof PlanesPlanIdDetalleRouteRouteWithChildren
'/planes/nuevo': typeof PlanesListaNuevoRoute '/planes/nuevo': typeof PlanesListaNuevoRoute
'/planes/$planId/asignaturas/$asignaturaId': typeof PlanesPlanIdAsignaturasAsignaturaIdRouteRoute '/planes/$planId/asignaturas/$asignaturaId': typeof PlanesPlanIdAsignaturasAsignaturaIdRouteRoute
@@ -182,6 +190,7 @@ export interface FileRoutesById {
'/login': typeof LoginRoute '/login': typeof LoginRoute
'/planes/_lista': typeof PlanesListaRouteRouteWithChildren '/planes/_lista': typeof PlanesListaRouteRouteWithChildren
'/demo/tanstack-query': typeof DemoTanstackQueryRoute '/demo/tanstack-query': typeof DemoTanstackQueryRoute
'/planes/PlanesListRoute': typeof PlanesPlanesListRouteRoute
'/planes/$planId/_detalle': typeof PlanesPlanIdDetalleRouteRouteWithChildren '/planes/$planId/_detalle': typeof PlanesPlanIdDetalleRouteRouteWithChildren
'/planes/$planId/asignaturas': typeof PlanesPlanIdAsignaturasRouteRouteWithChildren '/planes/$planId/asignaturas': typeof PlanesPlanIdAsignaturasRouteRouteWithChildren
'/planes/_lista/nuevo': typeof PlanesListaNuevoRoute '/planes/_lista/nuevo': typeof PlanesListaNuevoRoute
@@ -205,6 +214,7 @@ export interface FileRouteTypes {
| '/login' | '/login'
| '/planes' | '/planes'
| '/demo/tanstack-query' | '/demo/tanstack-query'
| '/planes/PlanesListRoute'
| '/planes/$planId' | '/planes/$planId'
| '/planes/$planId/asignaturas' | '/planes/$planId/asignaturas'
| '/planes/nuevo' | '/planes/nuevo'
@@ -225,6 +235,7 @@ export interface FileRouteTypes {
| '/login' | '/login'
| '/planes' | '/planes'
| '/demo/tanstack-query' | '/demo/tanstack-query'
| '/planes/PlanesListRoute'
| '/planes/$planId' | '/planes/$planId'
| '/planes/nuevo' | '/planes/nuevo'
| '/planes/$planId/asignaturas/$asignaturaId' | '/planes/$planId/asignaturas/$asignaturaId'
@@ -244,6 +255,7 @@ export interface FileRouteTypes {
| '/login' | '/login'
| '/planes/_lista' | '/planes/_lista'
| '/demo/tanstack-query' | '/demo/tanstack-query'
| '/planes/PlanesListRoute'
| '/planes/$planId/_detalle' | '/planes/$planId/_detalle'
| '/planes/$planId/asignaturas' | '/planes/$planId/asignaturas'
| '/planes/_lista/nuevo' | '/planes/_lista/nuevo'
@@ -266,6 +278,7 @@ export interface RootRouteChildren {
LoginRoute: typeof LoginRoute LoginRoute: typeof LoginRoute
PlanesListaRouteRoute: typeof PlanesListaRouteRouteWithChildren PlanesListaRouteRoute: typeof PlanesListaRouteRouteWithChildren
DemoTanstackQueryRoute: typeof DemoTanstackQueryRoute DemoTanstackQueryRoute: typeof DemoTanstackQueryRoute
PlanesPlanesListRouteRoute: typeof PlanesPlanesListRouteRoute
PlanesPlanIdDetalleRouteRoute: typeof PlanesPlanIdDetalleRouteRouteWithChildren PlanesPlanIdDetalleRouteRoute: typeof PlanesPlanIdDetalleRouteRouteWithChildren
PlanesPlanIdAsignaturasRouteRoute: typeof PlanesPlanIdAsignaturasRouteRouteWithChildren PlanesPlanIdAsignaturasRouteRoute: typeof PlanesPlanIdAsignaturasRouteRouteWithChildren
} }
@@ -293,6 +306,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof IndexRouteImport preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport parentRoute: typeof rootRouteImport
} }
'/planes/PlanesListRoute': {
id: '/planes/PlanesListRoute'
path: '/planes/PlanesListRoute'
fullPath: '/planes/PlanesListRoute'
preLoaderRoute: typeof PlanesPlanesListRouteRouteImport
parentRoute: typeof rootRouteImport
}
'/demo/tanstack-query': { '/demo/tanstack-query': {
id: '/demo/tanstack-query' id: '/demo/tanstack-query'
path: '/demo/tanstack-query' path: '/demo/tanstack-query'
@@ -486,6 +506,7 @@ const rootRouteChildren: RootRouteChildren = {
LoginRoute: LoginRoute, LoginRoute: LoginRoute,
PlanesListaRouteRoute: PlanesListaRouteRouteWithChildren, PlanesListaRouteRoute: PlanesListaRouteRouteWithChildren,
DemoTanstackQueryRoute: DemoTanstackQueryRoute, DemoTanstackQueryRoute: DemoTanstackQueryRoute,
PlanesPlanesListRouteRoute: PlanesPlanesListRouteRoute,
PlanesPlanIdDetalleRouteRoute: PlanesPlanIdDetalleRouteRouteWithChildren, PlanesPlanIdDetalleRouteRoute: PlanesPlanIdDetalleRouteRouteWithChildren,
PlanesPlanIdAsignaturasRouteRoute: PlanesPlanIdAsignaturasRouteRoute:
PlanesPlanIdAsignaturasRouteRouteWithChildren, PlanesPlanIdAsignaturasRouteRouteWithChildren,

View File

@@ -0,0 +1,42 @@
import { createFileRoute } from '@tanstack/react-router'
import { useMemo, useState } from 'react'
import { usePlanes } from '@/data'
export const Route = createFileRoute('/planes/PlanesListRoute')({
component: RouteComponent,
})
function RouteComponent() {
const [search, setSearch] = useState('')
const filters = useMemo(
() => ({ search, limit: 20, offset: 0, activo: true }),
[search],
)
const { data, isLoading, isError, error } = usePlanes(filters)
return (
<div style={{ padding: 16 }}>
<h1>Planes</h1>
<input
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Buscar…"
/>
{isLoading && <div>Cargando</div>}
{isError && <div>Error: {(error as any).message}</div>}
<ul>
{(data?.data ?? []).map((p) => (
<li key={p.id}>
<pre>{JSON.stringify(p, null, 2)}</pre>
</li>
))}
</ul>
</div>
)
}

BIN
src/types/supabase.ts Normal file

Binary file not shown.

View File

@@ -0,0 +1 @@
v2.67.1

View File

@@ -0,0 +1 @@
v2.184.1

View File

@@ -0,0 +1 @@
postgresql://postgres.exdkssurzmjnnhgtiama@aws-0-us-west-1.pooler.supabase.com:5432/postgres

View File

@@ -0,0 +1 @@
15.8.1.085

View File

@@ -0,0 +1 @@
exdkssurzmjnnhgtiama

View File

@@ -0,0 +1 @@
v12.2.3

View File

@@ -0,0 +1 @@
buckets-objects-grants-postgres

View File

@@ -0,0 +1 @@
v1.33.0