From 9727f4c6919498c1adb3c0246d538cc059636287 Mon Sep 17 00:00:00 2001 From: Alejandro Rosales Date: Fri, 22 Aug 2025 08:10:49 -0600 Subject: [PATCH] feat: implement asignaturas management with dynamic routing and UI updates --- src/main.tsx | 4 +- src/routeTree.gen.ts | 84 +++-------- src/routes/_authenticated/asignaturas.tsx | 9 -- .../_authenticated/asignaturas/$planId.tsx | 142 ++++++++++++++++++ .../_authenticated/facultad/$facultadId.tsx | 22 +-- src/routes/_authenticated/plan/$planId.tsx | 86 ++++++++--- .../_authenticated/plan/$planId/modal.tsx | 112 -------------- src/routes/_authenticated/planes.tsx | 2 +- 8 files changed, 249 insertions(+), 212 deletions(-) delete mode 100644 src/routes/_authenticated/asignaturas.tsx create mode 100644 src/routes/_authenticated/asignaturas/$planId.tsx delete mode 100644 src/routes/_authenticated/plan/$planId/modal.tsx diff --git a/src/main.tsx b/src/main.tsx index b17f59a..5c26aa6 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,6 +1,6 @@ import { StrictMode } from 'react' import ReactDOM from 'react-dom/client' -import { RouterProvider, createRouter } from '@tanstack/react-router' +import { RouterProvider, createRouteMask, createRouter } from '@tanstack/react-router' // Import the generated route tree import { routeTree } from './routeTree.gen' @@ -15,7 +15,7 @@ const router = createRouter({ scrollRestoration: true, defaultStructuralSharing: true, defaultPreloadStaleTime: 0, - context:{ + context: { auth: undefined!, }, }) diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index f7a84b6..afa473e 100644 --- a/src/routeTree.gen.ts +++ b/src/routeTree.gen.ts @@ -16,10 +16,9 @@ import { Route as AuthenticatedUsuariosRouteImport } from './routes/_authenticat import { Route as AuthenticatedPlanesRouteImport } from './routes/_authenticated/planes' import { Route as AuthenticatedFacultadesRouteImport } from './routes/_authenticated/facultades' import { Route as AuthenticatedDashboardRouteImport } from './routes/_authenticated/dashboard' -import { Route as AuthenticatedAsignaturasRouteImport } from './routes/_authenticated/asignaturas' import { Route as AuthenticatedPlanPlanIdRouteImport } from './routes/_authenticated/plan/$planId' import { Route as AuthenticatedFacultadFacultadIdRouteImport } from './routes/_authenticated/facultad/$facultadId' -import { Route as AuthenticatedPlanPlanIdModalRouteImport } from './routes/_authenticated/plan/$planId/modal' +import { Route as AuthenticatedAsignaturasPlanIdRouteImport } from './routes/_authenticated/asignaturas/$planId' const LoginRoute = LoginRouteImport.update({ id: '/login', @@ -55,12 +54,6 @@ const AuthenticatedDashboardRoute = AuthenticatedDashboardRouteImport.update({ path: '/dashboard', getParentRoute: () => AuthenticatedRoute, } as any) -const AuthenticatedAsignaturasRoute = - AuthenticatedAsignaturasRouteImport.update({ - id: '/asignaturas', - path: '/asignaturas', - getParentRoute: () => AuthenticatedRoute, - } as any) const AuthenticatedPlanPlanIdRoute = AuthenticatedPlanPlanIdRouteImport.update({ id: '/plan/$planId', path: '/plan/$planId', @@ -72,89 +65,83 @@ const AuthenticatedFacultadFacultadIdRoute = path: '/facultad/$facultadId', getParentRoute: () => AuthenticatedRoute, } as any) -const AuthenticatedPlanPlanIdModalRoute = - AuthenticatedPlanPlanIdModalRouteImport.update({ - id: '/modal', - path: '/modal', - getParentRoute: () => AuthenticatedPlanPlanIdRoute, +const AuthenticatedAsignaturasPlanIdRoute = + AuthenticatedAsignaturasPlanIdRouteImport.update({ + id: '/asignaturas/$planId', + path: '/asignaturas/$planId', + getParentRoute: () => AuthenticatedRoute, } as any) export interface FileRoutesByFullPath { '/': typeof IndexRoute '/login': typeof LoginRoute - '/asignaturas': typeof AuthenticatedAsignaturasRoute '/dashboard': typeof AuthenticatedDashboardRoute '/facultades': typeof AuthenticatedFacultadesRoute '/planes': typeof AuthenticatedPlanesRoute '/usuarios': typeof AuthenticatedUsuariosRoute + '/asignaturas/$planId': typeof AuthenticatedAsignaturasPlanIdRoute '/facultad/$facultadId': typeof AuthenticatedFacultadFacultadIdRoute - '/plan/$planId': typeof AuthenticatedPlanPlanIdRouteWithChildren - '/plan/$planId/modal': typeof AuthenticatedPlanPlanIdModalRoute + '/plan/$planId': typeof AuthenticatedPlanPlanIdRoute } export interface FileRoutesByTo { '/': typeof IndexRoute '/login': typeof LoginRoute - '/asignaturas': typeof AuthenticatedAsignaturasRoute '/dashboard': typeof AuthenticatedDashboardRoute '/facultades': typeof AuthenticatedFacultadesRoute '/planes': typeof AuthenticatedPlanesRoute '/usuarios': typeof AuthenticatedUsuariosRoute + '/asignaturas/$planId': typeof AuthenticatedAsignaturasPlanIdRoute '/facultad/$facultadId': typeof AuthenticatedFacultadFacultadIdRoute - '/plan/$planId': typeof AuthenticatedPlanPlanIdRouteWithChildren - '/plan/$planId/modal': typeof AuthenticatedPlanPlanIdModalRoute + '/plan/$planId': typeof AuthenticatedPlanPlanIdRoute } export interface FileRoutesById { __root__: typeof rootRouteImport '/': typeof IndexRoute '/_authenticated': typeof AuthenticatedRouteWithChildren '/login': typeof LoginRoute - '/_authenticated/asignaturas': typeof AuthenticatedAsignaturasRoute '/_authenticated/dashboard': typeof AuthenticatedDashboardRoute '/_authenticated/facultades': typeof AuthenticatedFacultadesRoute '/_authenticated/planes': typeof AuthenticatedPlanesRoute '/_authenticated/usuarios': typeof AuthenticatedUsuariosRoute + '/_authenticated/asignaturas/$planId': typeof AuthenticatedAsignaturasPlanIdRoute '/_authenticated/facultad/$facultadId': typeof AuthenticatedFacultadFacultadIdRoute - '/_authenticated/plan/$planId': typeof AuthenticatedPlanPlanIdRouteWithChildren - '/_authenticated/plan/$planId/modal': typeof AuthenticatedPlanPlanIdModalRoute + '/_authenticated/plan/$planId': typeof AuthenticatedPlanPlanIdRoute } export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath fullPaths: | '/' | '/login' - | '/asignaturas' | '/dashboard' | '/facultades' | '/planes' | '/usuarios' + | '/asignaturas/$planId' | '/facultad/$facultadId' | '/plan/$planId' - | '/plan/$planId/modal' fileRoutesByTo: FileRoutesByTo to: | '/' | '/login' - | '/asignaturas' | '/dashboard' | '/facultades' | '/planes' | '/usuarios' + | '/asignaturas/$planId' | '/facultad/$facultadId' | '/plan/$planId' - | '/plan/$planId/modal' id: | '__root__' | '/' | '/_authenticated' | '/login' - | '/_authenticated/asignaturas' | '/_authenticated/dashboard' | '/_authenticated/facultades' | '/_authenticated/planes' | '/_authenticated/usuarios' + | '/_authenticated/asignaturas/$planId' | '/_authenticated/facultad/$facultadId' | '/_authenticated/plan/$planId' - | '/_authenticated/plan/$planId/modal' fileRoutesById: FileRoutesById } export interface RootRouteChildren { @@ -214,13 +201,6 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof AuthenticatedDashboardRouteImport parentRoute: typeof AuthenticatedRoute } - '/_authenticated/asignaturas': { - id: '/_authenticated/asignaturas' - path: '/asignaturas' - fullPath: '/asignaturas' - preLoaderRoute: typeof AuthenticatedAsignaturasRouteImport - parentRoute: typeof AuthenticatedRoute - } '/_authenticated/plan/$planId': { id: '/_authenticated/plan/$planId' path: '/plan/$planId' @@ -235,48 +215,34 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof AuthenticatedFacultadFacultadIdRouteImport parentRoute: typeof AuthenticatedRoute } - '/_authenticated/plan/$planId/modal': { - id: '/_authenticated/plan/$planId/modal' - path: '/modal' - fullPath: '/plan/$planId/modal' - preLoaderRoute: typeof AuthenticatedPlanPlanIdModalRouteImport - parentRoute: typeof AuthenticatedPlanPlanIdRoute + '/_authenticated/asignaturas/$planId': { + id: '/_authenticated/asignaturas/$planId' + path: '/asignaturas/$planId' + fullPath: '/asignaturas/$planId' + preLoaderRoute: typeof AuthenticatedAsignaturasPlanIdRouteImport + parentRoute: typeof AuthenticatedRoute } } } -interface AuthenticatedPlanPlanIdRouteChildren { - AuthenticatedPlanPlanIdModalRoute: typeof AuthenticatedPlanPlanIdModalRoute -} - -const AuthenticatedPlanPlanIdRouteChildren: AuthenticatedPlanPlanIdRouteChildren = - { - AuthenticatedPlanPlanIdModalRoute: AuthenticatedPlanPlanIdModalRoute, - } - -const AuthenticatedPlanPlanIdRouteWithChildren = - AuthenticatedPlanPlanIdRoute._addFileChildren( - AuthenticatedPlanPlanIdRouteChildren, - ) - interface AuthenticatedRouteChildren { - AuthenticatedAsignaturasRoute: typeof AuthenticatedAsignaturasRoute AuthenticatedDashboardRoute: typeof AuthenticatedDashboardRoute AuthenticatedFacultadesRoute: typeof AuthenticatedFacultadesRoute AuthenticatedPlanesRoute: typeof AuthenticatedPlanesRoute AuthenticatedUsuariosRoute: typeof AuthenticatedUsuariosRoute + AuthenticatedAsignaturasPlanIdRoute: typeof AuthenticatedAsignaturasPlanIdRoute AuthenticatedFacultadFacultadIdRoute: typeof AuthenticatedFacultadFacultadIdRoute - AuthenticatedPlanPlanIdRoute: typeof AuthenticatedPlanPlanIdRouteWithChildren + AuthenticatedPlanPlanIdRoute: typeof AuthenticatedPlanPlanIdRoute } const AuthenticatedRouteChildren: AuthenticatedRouteChildren = { - AuthenticatedAsignaturasRoute: AuthenticatedAsignaturasRoute, AuthenticatedDashboardRoute: AuthenticatedDashboardRoute, AuthenticatedFacultadesRoute: AuthenticatedFacultadesRoute, AuthenticatedPlanesRoute: AuthenticatedPlanesRoute, AuthenticatedUsuariosRoute: AuthenticatedUsuariosRoute, + AuthenticatedAsignaturasPlanIdRoute: AuthenticatedAsignaturasPlanIdRoute, AuthenticatedFacultadFacultadIdRoute: AuthenticatedFacultadFacultadIdRoute, - AuthenticatedPlanPlanIdRoute: AuthenticatedPlanPlanIdRouteWithChildren, + AuthenticatedPlanPlanIdRoute: AuthenticatedPlanPlanIdRoute, } const AuthenticatedRouteWithChildren = AuthenticatedRoute._addFileChildren( diff --git a/src/routes/_authenticated/asignaturas.tsx b/src/routes/_authenticated/asignaturas.tsx deleted file mode 100644 index d0d0c93..0000000 --- a/src/routes/_authenticated/asignaturas.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { createFileRoute } from '@tanstack/react-router' - -export const Route = createFileRoute('/_authenticated/asignaturas')({ - component: RouteComponent, -}) - -function RouteComponent() { - return
Hello "/_authenticated/asignaturas"!
-} diff --git a/src/routes/_authenticated/asignaturas/$planId.tsx b/src/routes/_authenticated/asignaturas/$planId.tsx new file mode 100644 index 0000000..861389f --- /dev/null +++ b/src/routes/_authenticated/asignaturas/$planId.tsx @@ -0,0 +1,142 @@ +import { createFileRoute, useRouter } from "@tanstack/react-router" +import { supabase } from "@/auth/supabase" +import * as Icons from "lucide-react" +import { useMemo, useState } from "react" +import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog" +import { Input } from "@/components/ui/input" +import { Badge } from "@/components/ui/badge" + +type Asignatura = { + id: string + nombre: string + semestre: number | null + creditos: number | null + horas_teoricas: number | null + horas_practicas: number | null +} + +type ModalData = { + planId: string + planNombre: string + asignaturas: Asignatura[] +} + +export const Route = createFileRoute("/_authenticated/asignaturas/$planId")({ + component: ModalComponent, + loader: async ({ params }): Promise => { + const planId = params.planId + + const { data: plan, error: planErr } = await supabase + .from("plan_estudios") + .select("id, nombre") + .eq("id", planId) + .single() + if (planErr || !plan) throw planErr ?? new Error("Plan no encontrado") + + const { data: asignaturas, error: aErr } = await supabase + .from("asignaturas") + .select("id, nombre, semestre, creditos, horas_teoricas, horas_practicas") + .eq("plan_id", planId) + .order("semestre", { ascending: true }) + .order("nombre", { ascending: true }) + + if (aErr) throw aErr + + return { + planId, + planNombre: plan.nombre, + asignaturas: (asignaturas ?? []) as Asignatura[], + } + }, +}) + +function ModalComponent() { + const { planId, planNombre, asignaturas } = Route.useLoaderData() as ModalData + const router = useRouter() + const [q, setQ] = useState("") + + const filtered = useMemo(() => { + const t = q.trim().toLowerCase() + if (!t) return asignaturas + return asignaturas.filter(a => + [a.nombre, a.semestre, a.creditos] + .filter(Boolean) + .some(v => String(v).toLowerCase().includes(t)) + ) + }, [q, asignaturas]) + + // Agrupar por semestre + const groups = useMemo(() => { + const m = new Map() + for (const a of filtered) { + const k = a.semestre ?? "—" + if (!m.has(k)) m.set(k, []) + m.get(k)!.push(a) + } + return Array.from(m.entries()).sort(([a], [b]) => { + if (a === "—") return 1 + if (b === "—") return -1 + return Number(a) - Number(b) + }) + }, [filtered]) + + return ( + + router.navigate({ to: "/plan/$planId", params: { planId }, replace: true }) + } + > + + + + + Asignaturas · {planNombre} + + + +
+ setQ(e.target.value)} + placeholder="Buscar por nombre, semestre…" + className="w-full" + /> +
+ +
+ {groups.length === 0 && ( +
Sin asignaturas
+ )} + +
+ {groups.map(([sem, items]) => ( +
+
+ Semestre {sem} +
+
    + {items.map(a => ( +
  • +
    {a.nombre}
    +
    + {a.creditos != null && ( + Créditos: {a.creditos} + )} + {(a.horas_teoricas ?? 0) + (a.horas_practicas ?? 0) > 0 && ( + + Hrs T/P: {a.horas_teoricas ?? 0}/{a.horas_practicas ?? 0} + + )} +
    +
  • + ))} +
+
+ ))} +
+
+
+
+ ) +} diff --git a/src/routes/_authenticated/facultad/$facultadId.tsx b/src/routes/_authenticated/facultad/$facultadId.tsx index b79dcd7..bebd4a0 100644 --- a/src/routes/_authenticated/facultad/$facultadId.tsx +++ b/src/routes/_authenticated/facultad/$facultadId.tsx @@ -5,6 +5,7 @@ import { createFileRoute, Link } from '@tanstack/react-router' import * as Icons from 'lucide-react' import { supabase } from '@/auth/supabase' import { useMemo } from 'react' +import { useTheme } from '@/components/theme-provider' type Facultad = { id: string; nombre: string; icon: string; color?: string | null } type Plan = { @@ -121,13 +122,14 @@ function gradientFrom(color?: string | null) { } // ====== UI helpers ====== -function ProgressRing({ pct }: { pct: number }) { +function ProgressRing({ pct, color }: { pct: number, color: string }) { const r = 42, c = 2 * Math.PI * r const offset = c * (1 - Math.min(Math.max(pct, 0), 100) / 100) + // Puedes ajustar el color del stroke según el tema return (
- + @@ -143,7 +145,7 @@ function ProgressRing({ pct }: { pct: number }) { function HealthItem({ label, value, to }: { label: string; value: number; to: string }) { const warn = value > 0 return ( - + {label} {value} @@ -158,7 +160,7 @@ function RouteComponent() { return (
{/* Header */} -
+
@@ -179,14 +181,14 @@ function RouteComponent() { {/* Calidad + Salud */}
-
+
Calidad de planes
- +
Considera objetivo general, perfiles, sistema de evaluación y créditos.
-
+
Salud de asignaturas
@@ -197,7 +199,7 @@ function RouteComponent() {
{/* Actividad reciente */} -
+
Actividad reciente
    {recientes.length === 0 &&
  • Sin actividad
  • } @@ -221,12 +223,12 @@ function RouteComponent() { // Tarjeta métrica (igual a tu StatTile) function Metric({ to, label, value, Icon }:{ to: string; label: string; value: number; Icon: any }) { return ( - +
    {label}
    {value}
    -
    +
    diff --git a/src/routes/_authenticated/plan/$planId.tsx b/src/routes/_authenticated/plan/$planId.tsx index 4da3ea6..d5cd0c7 100644 --- a/src/routes/_authenticated/plan/$planId.tsx +++ b/src/routes/_authenticated/plan/$planId.tsx @@ -1,4 +1,4 @@ -import { createFileRoute, Link, useParams } from '@tanstack/react-router' +import { createFileRoute, Link } from '@tanstack/react-router' import { supabase, useSupabaseAuth } from '@/auth/supabase' import * as Icons from 'lucide-react' import { useEffect, useMemo, useRef, useState } from 'react' @@ -25,7 +25,8 @@ type PlanFull = { estado: string | null; fecha_creacion: string | null; carreras: { id: string; nombre: string; facultades?: { id: string; nombre: string; color?: string | null; icon?: string | null } | null } | null } -type LoaderData = { plan: PlanFull; asignaturasCount: number } +type AsignaturaLite = { id: string; nombre: string; semestre: number | null; creditos: number | null } +type LoaderData = { plan: PlanFull; asignaturasCount: number; asignaturasPreview: AsignaturaLite[] } /* ============== ROUTE ============== */ export const Route = createFileRoute('/_authenticated/plan/$planId')({ @@ -49,7 +50,19 @@ export const Route = createFileRoute('/_authenticated/plan/$planId')({ .select('*', { count: 'exact', head: true }) .eq('plan_id', params.planId) - return { plan: plan as unknown as PlanFull, asignaturasCount: count ?? 0 } + const { data: asignaturasPreview } = await supabase + .from('asignaturas') + .select('id, nombre, semestre, creditos') + .eq('plan_id', params.planId) + .order('semestre', { ascending: true }) + .order('nombre', { ascending: true }) + .limit(8) + + return { + plan: plan as unknown as PlanFull, + asignaturasCount: count ?? 0, + asignaturasPreview: (asignaturasPreview ?? []) as AsignaturaLite[], + } }, }) @@ -112,7 +125,7 @@ function GradientMesh({ color }: { color?: string | null }) { /* ============== PAGE ============== */ function RouteComponent() { - const { plan, asignaturasCount } = Route.useLoaderData() as LoaderData + const { plan, asignaturasCount, asignaturasPreview } = Route.useLoaderData() as LoaderData const auth = useSupabaseAuth() const showFacultad = auth.claims?.role === 'lci' || auth.claims?.role === 'vicerrectoria' const showCarrera = auth.claims?.role === 'secretario_academico' @@ -144,7 +157,7 @@ function RouteComponent() { // Stats y campos con ScrollTrigger if (statsRef.current) { const ctx = gsap.context(() => { - gsap.from('.kv', { + gsap.from('.academics', { y: 14, opacity: 0, stagger: .08, duration: .4, scrollTrigger: { trigger: statsRef.current, start: 'top 85%' } }) @@ -204,8 +217,8 @@ function RouteComponent() { )} Ver asignaturas @@ -220,23 +233,58 @@ function RouteComponent() { {/* stats */} - - - - - +
    + + + + + +
    +
    - +
    + +
    + + + Asignaturas ({asignaturasCount}) + + {/* Abre el modal enmascarado */} + + Ver todas + + + + + {asignaturasPreview.length === 0 && ( +
    Sin asignaturas
    + )} + {asignaturasPreview.map(a => ( + + {a.semestre ? `S${a.semestre} · ` : ''}{a.nombre} + + ))} +
    +
    ) } diff --git a/src/routes/_authenticated/plan/$planId/modal.tsx b/src/routes/_authenticated/plan/$planId/modal.tsx deleted file mode 100644 index 486b3e4..0000000 --- a/src/routes/_authenticated/plan/$planId/modal.tsx +++ /dev/null @@ -1,112 +0,0 @@ -import { createFileRoute, useRouter } from "@tanstack/react-router" -import { supabase } from "@/auth/supabase" -import * as Icons from "lucide-react" - -import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog" -import { Badge } from "@/components/ui/badge" - - -type PlanDetail = { - id: string - nombre: string - nivel: string | null - duracion: string | null - total_creditos: number | null - estado: string | null - carreras: { - id: string - nombre: string - facultades?: { id: string; nombre: string; color?: string | null; icon?: string | null } | null - } | null -} - -export const Route = createFileRoute('/_authenticated/plan/$planId/modal')({ - component: RouteComponent, - loader: async ({ params }) => { - const { data, error } = await supabase - .from('plan_estudios') - .select(` - id, nombre, nivel, duracion, total_creditos, estado, - carreras ( id, nombre, facultades:facultades ( id, nombre, color, icon ) ) - `) - .eq('id', params.planId) - .single() - if (error) throw error - return data - }, -}) - -function gradientFrom(color?: string | null) { - const base = (color && /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(color)) ? color : "#2563eb" - return `linear-gradient(135deg, ${base} 0%, ${base}CC 45%, ${base}99 75%, ${base}66 100%)` -} - -function RouteComponent() { - const plan = Route.useLoaderData() as PlanDetail - const router = useRouter() - const fac = plan.carreras?.facultades - const IconComp = (fac?.icon && (Icons as any)[fac.icon]) || Icons.Building2 - const headerBg = { background: gradientFrom(fac?.color) } - - return ( - - router.navigate({ - to: '/plan/$planId', - params: { planId: plan.id }, - replace: true, - }) - } - > - - {/* Header con color/ícono de facultad */} -
    -
    - - - -
    - - {plan.nombre} - -
    - {plan.carreras?.nombre ?? "—"} {fac?.nombre ? `· ${fac?.nombre}` : ""} -
    -
    - {plan.estado && ( - - {plan.estado} - - )} -
    -
    - - {/* Cuerpo */} -
    -
    -
    Nivel: {plan.nivel ?? "—"}
    -
    Duración: {plan.duracion ?? "—"}
    -
    Créditos: {plan.total_creditos ?? "—"}
    -
    Facultad: {fac?.nombre ?? "—"}
    -
    - - -
    -
    -
    - ) -} diff --git a/src/routes/_authenticated/planes.tsx b/src/routes/_authenticated/planes.tsx index f0b2ae6..d108365 100644 --- a/src/routes/_authenticated/planes.tsx +++ b/src/routes/_authenticated/planes.tsx @@ -122,7 +122,7 @@ function RouteComponent() { return (