From 53502d927b6694a7aff4a0ab892786819c5c5223 Mon Sep 17 00:00:00 2001 From: "Roberto.silva" Date: Wed, 29 Oct 2025 14:43:19 -0600 Subject: [PATCH 1/4] Se agregan filros por carrera y facultad --- src/routes/_authenticated/asignaturas.tsx | 118 ++++++++++++++++------ 1 file changed, 87 insertions(+), 31 deletions(-) diff --git a/src/routes/_authenticated/asignaturas.tsx b/src/routes/_authenticated/asignaturas.tsx index 61bf08b..b6a1160 100644 --- a/src/routes/_authenticated/asignaturas.tsx +++ b/src/routes/_authenticated/asignaturas.tsx @@ -169,6 +169,36 @@ function RouteComponent() { const [groupBy, setGroupBy] = useState<'semestre' | 'ninguno'>('semestre') const [flag, setFlag] = useState<'' | 'sinBibliografia' | 'sinCriterios' | 'sinContenidos'>(search.f ?? '') +const [facultad, setFacultad] = useState("todas") +const [carrera, setCarrera] = useState("todas") + + +// 🟣 Lista única de facultades +const facultadesList = useMemo(() => { + const unique = new Map() + planes?.forEach((p) => { + const fac = p.carrera?.facultad + if (fac?.id && fac?.nombre) unique.set(fac.id, fac.nombre) + }) + return Array.from(unique.entries()) +}, [planes]) + +// 🎓 Lista de carreras según la facultad seleccionada +const carrerasList = useMemo(() => { + const unique = new Map() + planes?.forEach((p) => { + if ( + p.carrera?.id && + p.carrera?.nombre && + (!facultad || facultad === "todas" || p.carrera?.facultad?.id === facultad) + ) { + unique.set(p.carrera.id, p.carrera.nombre) + } + }) + return Array.from(unique.entries()) +}, [planes, facultad]) + + // NEW: Clonado individual const [cloneOpen, setCloneOpen] = useState(false) const [cloneTarget, setCloneTarget] = useState(null) @@ -217,28 +247,30 @@ function RouteComponent() { return { sinBibliografia, sinCriterios, sinContenidos } }, [asignaturas]) - // Filtrado const filtered = useMemo(() => { - const t = q.trim().toLowerCase() - return asignaturas.filter(a => { - const matchesQ = - !t || - [a.nombre, a.clave, a.tipo, a.objetivos, a.plan?.nombre, a.plan?.carrera?.nombre, a.plan?.carrera?.facultad?.nombre] - .filter(Boolean) - .some(v => String(v).toLowerCase().includes(t)) + const t = q.trim().toLowerCase() + return asignaturas.filter(a => { + const matchesQ = + !t || + [a.nombre, a.clave, a.tipo, a.objetivos, a.plan?.nombre, a.plan?.carrera?.nombre, a.plan?.carrera?.facultad?.nombre] + .filter(Boolean) + .some(v => String(v).toLowerCase().includes(t)) - const semOK = sem === 'todos' || String(a.semestre ?? '—') === sem - const tipoOK = tipo === 'todos' || (a.tipo ?? '—') === tipo + const semOK = sem === 'todos' || String(a.semestre ?? '—') === sem + const tipoOK = tipo === 'todos' || (a.tipo ?? '—') === tipo + const carreraOK = carrera === 'todas' || a.plan?.carrera?.id === carrera + const facultadOK = facultad === 'todas' || a.plan?.carrera?.facultad?.id === facultad - const flagOK = - !flag || - (flag === 'sinBibliografia' && (!a.bibliografia || a.bibliografia.length === 0)) || - (flag === 'sinCriterios' && (!a.criterios_evaluacion || !a.criterios_evaluacion.trim())) || - (flag === 'sinContenidos' && (!a.contenidos || Object.keys(a.contenidos ?? {}).length === 0)) + const flagOK = + !flag || + (flag === 'sinBibliografia' && (!a.bibliografia || a.bibliografia.length === 0)) || + (flag === 'sinCriterios' && (!a.criterios_evaluacion || !a.criterios_evaluacion.trim())) || + (flag === 'sinContenidos' && (!a.contenidos || Object.keys(a.contenidos ?? {}).length === 0)) + + return matchesQ && semOK && tipoOK && flagOK && carreraOK && facultadOK + }) +}, [q, sem, tipo, flag, carrera, facultad, asignaturas]) - return matchesQ && semOK && tipoOK && flagOK - }) - }, [q, sem, tipo, flag, asignaturas]) // Agrupación const groups = useMemo(() => { @@ -257,7 +289,7 @@ function RouteComponent() { }, [filtered, groupBy]) // Helpers - const clearFilters = () => { setQ(''); setSem('todos'); setTipo('todos'); setFlag('') } + const clearFilters = () => { setQ(''); setSem('todos'); setTipo('todos'); setCarrera('todas'); setFlag('') } // NEW: util para clonar 1 asignatura async function cloneOne(src: Asignatura, overrides: { @@ -394,7 +426,7 @@ function RouteComponent() { {/* Filtros */} -
+
- - { + setFacultad(val) + setCarrera("todas") // reset de carrera al cambiar facultad + }} + > + + + + + Todas las facultades + {facultadesList.map(([id, nombre]) => ( + + {nombre} + + ))}
+ {facultad && facultad !== "todas" && (
- - setCarrera(val)} + > + + + - Por semestre - Sin agrupación + Todas las carreras + {carrerasList.map(([id, nombre]) => ( + + {nombre} + + ))}
+ )}
+ {/* Chips de salud */}
Date: Wed, 29 Oct 2025 14:44:47 -0600 Subject: [PATCH 2/4] Se agrgan filtros --- src/routes/_authenticated/planes.tsx | 205 +++++++++++++++++++++------ 1 file changed, 161 insertions(+), 44 deletions(-) diff --git a/src/routes/_authenticated/planes.tsx b/src/routes/_authenticated/planes.tsx index 1073570..ef3526e 100644 --- a/src/routes/_authenticated/planes.tsx +++ b/src/routes/_authenticated/planes.tsx @@ -10,26 +10,44 @@ import { Plus, RefreshCcw, Building2 } from "lucide-react" import { InfoChip } from "@/components/planes/InfoChip" import { CreatePlanDialog } from "@/components/planes/CreatePlanDialog" import { chipTint } from "@/components/planes/chipTint" -import { z } from 'zod' - - +import { z } from "zod" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" export type PlanDeEstudios = { - id: string; nombre: string; nivel: string | null; duracion: string | null; - total_creditos: number | null; estado: string | null; fecha_creacion: string | null; carrera_id: string | null + id: string + nombre: string + nivel: string | null + duracion: string | null + total_creditos: number | null + estado: string | null + fecha_creacion: string | null + carrera_id: string | null } type PlanRow = PlanDeEstudios & { carreras: { - id: string; nombre: string; - facultades?: { id: string; nombre: string; color?: string | null; icon?: string | null } | null + id: string + nombre: string + facultades?: { + id: string + nombre: string + color?: string | null + icon?: string | null + } | null } | null } const planSearchSchema = z.object({ - plan: z.string().nullable() + plan: z.string().nullable(), + facultad: z.string().nullable().optional(), + carrera: z.string().nullable().optional(), }) - export const Route = createFileRoute("/_authenticated/planes")({ component: RouteComponent, loader: async () => { @@ -45,93 +63,191 @@ export const Route = createFileRoute("/_authenticated/planes")({ `) .order("fecha_creacion", { ascending: false }) .limit(100) - console.log({ data, error }) if (error) throw new Error(error.message) return (data ?? []) as PlanRow[] }, validateSearch: planSearchSchema, }) - function RouteComponent() { const auth = useSupabaseAuth() - const { plan } = Route.useSearch() + const { plan, facultad, carrera } = Route.useSearch() const [openCreate, setOpenCreate] = useState(false) const data = Route.useLoaderData() as PlanRow[] const router = useRouter() const navigate = useNavigate({ from: Route.fullPath }) + const showFacultad = + auth.claims?.role === "lci" || auth.claims?.role === "vicerrectoria" + const showCarrera = + showFacultad || auth.claims?.role === "secretario_academico" - const showFacultad = auth.claims?.role === "lci" || auth.claims?.role === "vicerrectoria" - const showCarrera = showFacultad || auth.claims?.role === "secretario_academico" + // 🟣 Lista única de facultades + const facultadesList = useMemo(() => { + const unique = new Map() + data?.forEach((p) => { + const fac = p.carreras?.facultades + if (fac?.id && fac?.nombre) unique.set(fac.id, fac.nombre) + }) + return Array.from(unique.entries()) + }, [data]) + // 🎓 Lista de carreras según facultad seleccionada + const carrerasList = useMemo(() => { + const unique = new Map() + data?.forEach((p) => { + if ( + p.carreras?.id && + p.carreras?.nombre && + (!facultad || p.carreras?.facultades?.id === facultad) + ) { + unique.set(p.carreras.id, p.carreras.nombre) + } + }) + return Array.from(unique.entries()) + }, [data, facultad]) + + // 🧩 Filtrado general const filtered = useMemo(() => { const term = plan?.trim().toLowerCase() - if (!term || !data) return data - return data.filter((p) => - [p.nombre, p.nivel, p.estado, p.carreras?.nombre, p.carreras?.facultades?.nombre] - .filter(Boolean) - .some((v) => String(v).toLowerCase().includes(term)) - ) - }, [plan, data]) + let results = data ?? [] + + if (term) { + results = results.filter((p) => + [p.nombre, p.nivel, p.estado, p.carreras?.nombre, p.carreras?.facultades?.nombre] + .filter(Boolean) + .some((v) => String(v).toLowerCase().includes(term)) + ) + } + + if (facultad && facultad !== "todas") { + results = results.filter((p) => p.carreras?.facultades?.id === facultad) + } + + if (carrera && carrera !== "todas") { + results = results.filter((p) => p.carreras?.id === carrera) + } + + return results + }, [plan, facultad, carrera, data]) return (
Planes de estudio -
+ +
+ {/* 🔍 Buscador */}
navigate({ search: { plan: e.target.value } })} + value={plan ?? ""} + onChange={(e) => + navigate({ search: { plan: e.target.value, facultad, carrera } }) + } placeholder="Buscar por nombre, nivel, estado…" />
- + + {/* ➕ Nuevo plan */} -
- {/* GRID de tarjetas con estilo suave por facultad */} + {/* GRID de tarjetas */}
{filtered?.map((p) => { const fac = p.carreras?.facultades const styles = chipTint(fac?.color) - const IconComp = (fac?.icon && (Icons as any)[fac.icon]) || Building2 + const IconComp = + (fac?.icon && (Icons as any)[fac.icon]) || Building2 return (
- +
{p.nombre}
- {p.nivel ?? "—"} {p.duracion ? `· ${p.duracion}` : ""} + {p.nivel ?? "—"}{" "} + {p.duracion ? `· ${p.duracion}` : ""}
- {/* Dentro del map de tarjetas, sustituye SOLO el footer inferior */}
- {/* grupo izquierdo: chips (wrap si no caben) */}
{showCarrera && p.carreras?.nombre && ( - {/* derecha: estado */} {p.estado && ( - {p.estado && p.estado.length > 10 ? `${p.estado.slice(0, 10)}…` : p.estado} + {p.estado.length > 10 + ? `${p.estado.slice(0, 10)}…` + : p.estado} )}
-
) @@ -167,16 +286,14 @@ function RouteComponent() {
{!filtered?.length && ( -
Sin resultados
+
+ Sin resultados +
)} - - +
) } From d25b8b04414c218b40f67f4e27123434182a0971 Mon Sep 17 00:00:00 2001 From: "Roberto.silva" Date: Fri, 24 Oct 2025 12:36:39 -0600 Subject: [PATCH 3/4] Se corrigen bugs sobre crear carreras, filtrado y que aparezcan las materias cuando se crean --- src/components/carreras/DeleteCarreras.tsx | 2 +- src/components/planes/AddAsignaturaButton.tsx | 19 ++++++++++++++++- src/routes/_authenticated/carreras.tsx | 21 ++++++++++++++----- src/routes/_authenticated/plan/$planId.tsx | 5 ++++- 4 files changed, 39 insertions(+), 8 deletions(-) diff --git a/src/components/carreras/DeleteCarreras.tsx b/src/components/carreras/DeleteCarreras.tsx index 85ebd65..2b181aa 100644 --- a/src/components/carreras/DeleteCarreras.tsx +++ b/src/components/carreras/DeleteCarreras.tsx @@ -30,7 +30,7 @@ export function useDeleteCarreraDialog(carreraId: string, onDeleted?: () => void const dialog = ( - + ¿Eliminar carrera? diff --git a/src/components/planes/AddAsignaturaButton.tsx b/src/components/planes/AddAsignaturaButton.tsx index fade9b3..c2b4e62 100644 --- a/src/components/planes/AddAsignaturaButton.tsx +++ b/src/components/planes/AddAsignaturaButton.tsx @@ -11,9 +11,11 @@ import { supabase, useSupabaseAuth } from "@/auth/supabase" import { Field } from "./Field" import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs" import { asignaturaKeys } from "./planQueries" +import { useRouter } from "@tanstack/react-router" export function AddAsignaturaButton({ planId, onAdded }: { planId: string; onAdded?: () => void }) { const qc = useQueryClient() + const router = useRouter() const supabaseAuth = useSupabaseAuth() const [open, setOpen] = useState(false) const [saving, setSaving] = useState(false) @@ -45,7 +47,13 @@ export function AddAsignaturaButton({ planId, onAdded }: { planId: string; onAdd objetivos: toNull(f.objetivos), contenidos: [], bibliografia: [], criterios_evaluacion: null, } - const { error } = await supabase.from("asignaturas").insert([payload]) + const { error,data } = await supabase.from("asignaturas").insert([payload]).select().single() + console.log(data); + router.invalidate() + router.navigate({ + to: "/asignatura/$asignaturaId", + params: { asignaturaId: data.id }, + }) setSaving(false) if (error) { alert(error.message); return } setOpen(false) @@ -64,8 +72,17 @@ export function AddAsignaturaButton({ planId, onAdded }: { planId: string; onAdd body: JSON.stringify({ planEstudiosId: planId, prompt: iaPrompt, semestre: iaSemestre.trim() ? Number(iaSemestre) : undefined, insert: true, uuid: supabaseAuth.user?.id }), }) if (!res.ok) throw new Error(await res.text()) + const data = await res.json() + console.log("Asignatura generada:", data) + const asignaturaId = data.asignaturaId || data.insertResult?.id + if (!asignaturaId) throw new Error("No se recibió el ID de la asignatura generada") confetti({ particleCount: 120, spread: 80, origin: { y: 0.6 } }) setOpen(false) + router.invalidate() + router.navigate({ + to: "/asignatura/$asignaturaId", + params: { asignaturaId }, + }) onAdded?.() // qc.invalidateQueries({ queryKey: asignaturaKeys.preview(planId) }) // qc.invalidateQueries({ queryKey: asignaturaKeys.count(planId) }) diff --git a/src/routes/_authenticated/carreras.tsx b/src/routes/_authenticated/carreras.tsx index 71a602c..57a98f1 100644 --- a/src/routes/_authenticated/carreras.tsx +++ b/src/routes/_authenticated/carreras.tsx @@ -126,6 +126,17 @@ function RouteComponent() { 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() @@ -198,10 +209,7 @@ function RouteComponent() { 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 - const { setOpen: setDeleteOpen, dialog: deleteDialog } = useDeleteCarreraDialog(c.id, async () => { - await qc.invalidateQueries({ queryKey: carrerasKeys.root }) - router.invalidate() - }) + return ( openContextMenu(e)}> @@ -233,7 +241,10 @@ function RouteComponent() { setEditCarrera(c)}> Editar - setDeleteOpen(true)}> + { + setDeleteTarget(c) + setDeleteOpen(true) + }}> Eliminar diff --git a/src/routes/_authenticated/plan/$planId.tsx b/src/routes/_authenticated/plan/$planId.tsx index 40bbc4e..fe51327 100644 --- a/src/routes/_authenticated/plan/$planId.tsx +++ b/src/routes/_authenticated/plan/$planId.tsx @@ -49,7 +49,10 @@ export const Route = createFileRoute("/_authenticated/plan/$planId")({ // ...existing code... function RouteComponent() { const qc = useQueryClient() - const { plan, asignaturas: asignaturasPreview } = Route.useLoaderData() as LoaderData + //const { plan, asignaturas: asignaturasPreview } = Route.useLoaderData() as LoaderData + const { plan } = Route.useLoaderData() as LoaderData + + const { data: asignaturasPreview } = useSuspenseQuery(asignaturasPreviewOptions(plan.id)) const auth = useSupabaseAuth() const asignaturasCount = asignaturasPreview.length From 4cf93ff1f43539762277878fb98f8efb41c47f9b Mon Sep 17 00:00:00 2001 From: Guillermo Arrieta Medina Date: Fri, 24 Oct 2025 13:01:33 -0600 Subject: [PATCH 4/4] =?UTF-8?q?La=20pantalla=20se=20volv=C3=ADa=20negra=20?= =?UTF-8?q?al=20abrir=20el=20dialogo=20de=20eliminar=20carrera?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit La razón es que se rendereaba un dialogo de borrado por carrera, pero al abrir uno se abrian los demás también --- src/routes/_authenticated/carreras.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/routes/_authenticated/carreras.tsx b/src/routes/_authenticated/carreras.tsx index 57a98f1..c549cc4 100644 --- a/src/routes/_authenticated/carreras.tsx +++ b/src/routes/_authenticated/carreras.tsx @@ -134,7 +134,7 @@ function RouteComponent() { async () => { await qc.invalidateQueries({ queryKey: carrerasKeys.root }) router.invalidate() - setDeleteTarget(null) + // setDeleteTarget(null) } ) @@ -248,7 +248,7 @@ function RouteComponent() { Eliminar - {deleteDialog} + ) })} @@ -258,6 +258,8 @@ function RouteComponent() {
+ {deleteDialog} + {/* Crear / Editar */}