// routes/_authenticated/carreras.tsx (refactor a TanStack Query v5) import { createFileRoute, useRouter } from "@tanstack/react-router" import { useMemo, useState } from "react" import { useSuspenseQuery, useQueryClient, queryOptions } from "@tanstack/react-query" import { supabase } from "@/auth/supabase" import * as Icons from "lucide-react" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { Input } from "@/components/ui/input" import { Button } from "@/components/ui/button" // import { Badge } from "@/components/ui/badge" // unused // import { Label } from "@/components/ui/label" // unused // import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog" // unused import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "@/components/ui/select" // import { Accordion, AccordionItem, AccordionTrigger, AccordionContent } from "@/components/ui/accordion" // unused // import { Switch } from "@/components/ui/switch" // unused import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuTrigger } from "@/components/ui/context-menu" import { useDeleteCarreraDialog } from "@/components/carreras/DeleteCarreras" // Modularized components import { CarreraFormDialog } from "@/components/carreras/CarreraFormDialog" import { CarreraDetailDialog } from "@/components/carreras/CarreraDetailDialog" import { StatusPill } from "@/components/carreras/StatusPill" import { tint } from "@/components/carreras/utils" import { openContextMenu } from "@/components/carreras/openContextMenu" /* -------------------- Tipos -------------------- */ export type FacultadLite = { id: string; nombre: string; color?: string | null; icon?: string | null } export type CarreraRow = { id: string nombre: string semestres: number activo: boolean facultad_id: string | null facultades?: FacultadLite | null } /* -------------------- Query Keys & Fetchers -------------------- */ export const carrerasKeys = { root: ["carreras"] as const, list: () => [...carrerasKeys.root, "list"] as const, } export const facultadesKeys = { root: ["facultades"] as const, all: () => [...facultadesKeys.root, "all"] as const, } export const criteriosKeys = { root: ["criterios_carrera"] as const, byCarrera: (id: string) => [...criteriosKeys.root, { carreraId: id }] as const, } async function fetchCarreras(): Promise { const { data, error } = await supabase .from("carreras") .select( `id, nombre, semestres, activo, facultad_id, facultades:facultades ( id, nombre, color, icon )` ) .order("nombre", { ascending: true }) if (error) throw error return (data ?? []) as unknown as CarreraRow[] } async function fetchFacultades(): Promise { const { data, error } = await supabase .from("facultades") .select("id, nombre, color, icon") .order("nombre", { ascending: true }) if (error) throw error return (data ?? []) as FacultadLite[] } export type CriterioRow = { id: number nombre: string descripcion: string | null tipo: string | null obligatorio: boolean | null referencia_documento: string | null fecha_creacion: string | null } async function fetchCriterios(carreraId: string): Promise { const { data, error } = await supabase .from("criterios_carrera") .select("id, nombre, descripcion, tipo, obligatorio, referencia_documento, fecha_creacion") .eq("carrera_id", carreraId) .order("fecha_creacion", { ascending: true }) if (error) throw error return (data ?? []) as CriterioRow[] } export const carrerasOptions = () => queryOptions({ queryKey: carrerasKeys.list(), queryFn: fetchCarreras, staleTime: 60_000 }) export const facultadesOptions = () => queryOptions({ queryKey: facultadesKeys.all(), queryFn: fetchFacultades, staleTime: 5 * 60_000 }) export const criteriosOptions = (carreraId: string) => queryOptions({ queryKey: criteriosKeys.byCarrera(carreraId), queryFn: () => fetchCriterios(carreraId) }) /* -------------------- Ruta -------------------- */ export const Route = createFileRoute("/_authenticated/carreras")({ component: RouteComponent, // Prefetch con TanStack Query (sin llamadas sueltas a Supabase fuera de QueryClient) loader: async ({ context: { queryClient } }) => { await Promise.all([ queryClient.ensureQueryData(carrerasOptions()), queryClient.ensureQueryData(facultadesOptions()), ]) return null }, }) // ...existing code... /* -------------------- Página -------------------- */ function RouteComponent() { const router = useRouter() const qc = useQueryClient() const { data: carreras } = useSuspenseQuery(carrerasOptions()) const { data: facultades } = useSuspenseQuery(facultadesOptions()) const [q, setQ] = useState("") const [fac, setFac] = useState("todas") const [state, setState] = useState<"todas" | "activas" | "inactivas">("todas") const [detail, setDetail] = useState(null) const [editCarrera, setEditCarrera] = useState(null) const [createOpen, setCreateOpen] = useState(false) const [deleteTarget, setDeleteTarget] = useState(null) // ✅ Se declara UNA SOLA VEZ const { setOpen: setDeleteOpen, dialog: deleteDialog } = useDeleteCarreraDialog( deleteTarget?.id ?? "", async () => { await qc.invalidateQueries({ queryKey: carrerasKeys.root }) router.invalidate() // setDeleteTarget(null) } ) const filtered = useMemo(() => { const term = q.trim().toLowerCase() return carreras.filter((c) => { if (fac !== "todas" && c.facultad_id !== fac) return false if (state === "activas" && !c.activo) return false if (state === "inactivas" && c.activo) return false if (!term) return true return [c.nombre, c.facultades?.nombre].filter(Boolean).some((v) => String(v).toLowerCase().includes(term)) }) }, [q, fac, state, carreras]) return (
Carreras
setQ(e.target.value)} placeholder="Buscar por nombre o facultad…" className="pl-8" />
{filtered.map((c) => { const fac = c.facultades const border = tint(fac?.color, 0.28) const chip = tint(fac?.color, 0.1) const IconComp = (fac?.icon && (Icons as any)[fac.icon]) || Icons.Building2 return ( openContextMenu(e)}>
{c.nombre}
{fac?.nombre ?? "—"} · {c.semestres} semestres
setDetail(c)}> Ver setEditCarrera(c)}> Editar { setDeleteTarget(c) setDeleteOpen(true) }}> Eliminar
) })}
{!filtered.length &&
No hay resultados
}
{deleteDialog} {/* Crear / Editar */} { await qc.invalidateQueries({ queryKey: carrerasKeys.root }) router.invalidate() }} /> !o && setEditCarrera(null)} facultades={facultades} mode="edit" carrera={editCarrera ?? undefined} onSaved={async () => { setEditCarrera(null) await qc.invalidateQueries({ queryKey: carrerasKeys.root }) router.invalidate() }} /> {/* Detalle + añadir criterio */} { await qc.invalidateQueries({ queryKey: carrerasKeys.root }) if (detail) await qc.invalidateQueries({ queryKey: criteriosKeys.byCarrera(detail.id) }) }} />
) } // ...existing code... // ...existing code... // ...existing code...