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 */}
{ @@ -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 +
)} - - +
) }