feat: Add dashboard and asignaturas routes with corresponding components

This commit is contained in:
2025-08-21 07:49:01 -06:00
parent 51faa98022
commit fe471bcfc2
7 changed files with 69 additions and 104 deletions

View File

@@ -14,6 +14,8 @@ import { Route as AuthenticatedRouteImport } from './routes/_authenticated'
import { Route as IndexRouteImport } from './routes/index' import { Route as IndexRouteImport } from './routes/index'
import { Route as AuthenticatedPlanesRouteImport } from './routes/_authenticated/planes' import { Route as AuthenticatedPlanesRouteImport } from './routes/_authenticated/planes'
import { Route as AuthenticatedFacultadesRouteImport } from './routes/_authenticated/facultades' 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 AuthenticatedPlanesPlanIdRouteImport } from './routes/_authenticated/planes/$planId' import { Route as AuthenticatedPlanesPlanIdRouteImport } from './routes/_authenticated/planes/$planId'
import { Route as AuthenticatedFacultadFacultadIdRouteImport } from './routes/_authenticated/facultad/$facultadId' import { Route as AuthenticatedFacultadFacultadIdRouteImport } from './routes/_authenticated/facultad/$facultadId'
import { Route as AuthenticatedPlanesPlanIdModalRouteImport } from './routes/_authenticated/planes/$planId/modal' import { Route as AuthenticatedPlanesPlanIdModalRouteImport } from './routes/_authenticated/planes/$planId/modal'
@@ -42,6 +44,17 @@ const AuthenticatedFacultadesRoute = AuthenticatedFacultadesRouteImport.update({
path: '/facultades', path: '/facultades',
getParentRoute: () => AuthenticatedRoute, getParentRoute: () => AuthenticatedRoute,
} as any) } as any)
const AuthenticatedDashboardRoute = AuthenticatedDashboardRouteImport.update({
id: '/dashboard',
path: '/dashboard',
getParentRoute: () => AuthenticatedRoute,
} as any)
const AuthenticatedAsignaturasRoute =
AuthenticatedAsignaturasRouteImport.update({
id: '/asignaturas',
path: '/asignaturas',
getParentRoute: () => AuthenticatedRoute,
} as any)
const AuthenticatedPlanesPlanIdRoute = const AuthenticatedPlanesPlanIdRoute =
AuthenticatedPlanesPlanIdRouteImport.update({ AuthenticatedPlanesPlanIdRouteImport.update({
id: '/$planId', id: '/$planId',
@@ -64,6 +77,8 @@ const AuthenticatedPlanesPlanIdModalRoute =
export interface FileRoutesByFullPath { export interface FileRoutesByFullPath {
'/': typeof IndexRoute '/': typeof IndexRoute
'/login': typeof LoginRoute '/login': typeof LoginRoute
'/asignaturas': typeof AuthenticatedAsignaturasRoute
'/dashboard': typeof AuthenticatedDashboardRoute
'/facultades': typeof AuthenticatedFacultadesRoute '/facultades': typeof AuthenticatedFacultadesRoute
'/planes': typeof AuthenticatedPlanesRouteWithChildren '/planes': typeof AuthenticatedPlanesRouteWithChildren
'/facultad/$facultadId': typeof AuthenticatedFacultadFacultadIdRoute '/facultad/$facultadId': typeof AuthenticatedFacultadFacultadIdRoute
@@ -73,6 +88,8 @@ export interface FileRoutesByFullPath {
export interface FileRoutesByTo { export interface FileRoutesByTo {
'/': typeof IndexRoute '/': typeof IndexRoute
'/login': typeof LoginRoute '/login': typeof LoginRoute
'/asignaturas': typeof AuthenticatedAsignaturasRoute
'/dashboard': typeof AuthenticatedDashboardRoute
'/facultades': typeof AuthenticatedFacultadesRoute '/facultades': typeof AuthenticatedFacultadesRoute
'/planes': typeof AuthenticatedPlanesRouteWithChildren '/planes': typeof AuthenticatedPlanesRouteWithChildren
'/facultad/$facultadId': typeof AuthenticatedFacultadFacultadIdRoute '/facultad/$facultadId': typeof AuthenticatedFacultadFacultadIdRoute
@@ -84,6 +101,8 @@ export interface FileRoutesById {
'/': typeof IndexRoute '/': typeof IndexRoute
'/_authenticated': typeof AuthenticatedRouteWithChildren '/_authenticated': typeof AuthenticatedRouteWithChildren
'/login': typeof LoginRoute '/login': typeof LoginRoute
'/_authenticated/asignaturas': typeof AuthenticatedAsignaturasRoute
'/_authenticated/dashboard': typeof AuthenticatedDashboardRoute
'/_authenticated/facultades': typeof AuthenticatedFacultadesRoute '/_authenticated/facultades': typeof AuthenticatedFacultadesRoute
'/_authenticated/planes': typeof AuthenticatedPlanesRouteWithChildren '/_authenticated/planes': typeof AuthenticatedPlanesRouteWithChildren
'/_authenticated/facultad/$facultadId': typeof AuthenticatedFacultadFacultadIdRoute '/_authenticated/facultad/$facultadId': typeof AuthenticatedFacultadFacultadIdRoute
@@ -95,6 +114,8 @@ export interface FileRouteTypes {
fullPaths: fullPaths:
| '/' | '/'
| '/login' | '/login'
| '/asignaturas'
| '/dashboard'
| '/facultades' | '/facultades'
| '/planes' | '/planes'
| '/facultad/$facultadId' | '/facultad/$facultadId'
@@ -104,6 +125,8 @@ export interface FileRouteTypes {
to: to:
| '/' | '/'
| '/login' | '/login'
| '/asignaturas'
| '/dashboard'
| '/facultades' | '/facultades'
| '/planes' | '/planes'
| '/facultad/$facultadId' | '/facultad/$facultadId'
@@ -114,6 +137,8 @@ export interface FileRouteTypes {
| '/' | '/'
| '/_authenticated' | '/_authenticated'
| '/login' | '/login'
| '/_authenticated/asignaturas'
| '/_authenticated/dashboard'
| '/_authenticated/facultades' | '/_authenticated/facultades'
| '/_authenticated/planes' | '/_authenticated/planes'
| '/_authenticated/facultad/$facultadId' | '/_authenticated/facultad/$facultadId'
@@ -164,6 +189,20 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof AuthenticatedFacultadesRouteImport preLoaderRoute: typeof AuthenticatedFacultadesRouteImport
parentRoute: typeof AuthenticatedRoute parentRoute: typeof AuthenticatedRoute
} }
'/_authenticated/dashboard': {
id: '/_authenticated/dashboard'
path: '/dashboard'
fullPath: '/dashboard'
preLoaderRoute: typeof AuthenticatedDashboardRouteImport
parentRoute: typeof AuthenticatedRoute
}
'/_authenticated/asignaturas': {
id: '/_authenticated/asignaturas'
path: '/asignaturas'
fullPath: '/asignaturas'
preLoaderRoute: typeof AuthenticatedAsignaturasRouteImport
parentRoute: typeof AuthenticatedRoute
}
'/_authenticated/planes/$planId': { '/_authenticated/planes/$planId': {
id: '/_authenticated/planes/$planId' id: '/_authenticated/planes/$planId'
path: '/$planId' path: '/$planId'
@@ -214,12 +253,16 @@ const AuthenticatedPlanesRouteWithChildren =
AuthenticatedPlanesRoute._addFileChildren(AuthenticatedPlanesRouteChildren) AuthenticatedPlanesRoute._addFileChildren(AuthenticatedPlanesRouteChildren)
interface AuthenticatedRouteChildren { interface AuthenticatedRouteChildren {
AuthenticatedAsignaturasRoute: typeof AuthenticatedAsignaturasRoute
AuthenticatedDashboardRoute: typeof AuthenticatedDashboardRoute
AuthenticatedFacultadesRoute: typeof AuthenticatedFacultadesRoute AuthenticatedFacultadesRoute: typeof AuthenticatedFacultadesRoute
AuthenticatedPlanesRoute: typeof AuthenticatedPlanesRouteWithChildren AuthenticatedPlanesRoute: typeof AuthenticatedPlanesRouteWithChildren
AuthenticatedFacultadFacultadIdRoute: typeof AuthenticatedFacultadFacultadIdRoute AuthenticatedFacultadFacultadIdRoute: typeof AuthenticatedFacultadFacultadIdRoute
} }
const AuthenticatedRouteChildren: AuthenticatedRouteChildren = { const AuthenticatedRouteChildren: AuthenticatedRouteChildren = {
AuthenticatedAsignaturasRoute: AuthenticatedAsignaturasRoute,
AuthenticatedDashboardRoute: AuthenticatedDashboardRoute,
AuthenticatedFacultadesRoute: AuthenticatedFacultadesRoute, AuthenticatedFacultadesRoute: AuthenticatedFacultadesRoute,
AuthenticatedPlanesRoute: AuthenticatedPlanesRouteWithChildren, AuthenticatedPlanesRoute: AuthenticatedPlanesRouteWithChildren,
AuthenticatedFacultadFacultadIdRoute: AuthenticatedFacultadFacultadIdRoute, AuthenticatedFacultadFacultadIdRoute: AuthenticatedFacultadFacultadIdRoute,

View File

@@ -22,7 +22,6 @@ import {
LayoutDashboard, LayoutDashboard,
GraduationCap, GraduationCap,
FileText, FileText,
Settings,
LogOut, LogOut,
KeySquare, KeySquare,
IdCard, IdCard,
@@ -40,9 +39,8 @@ export const Route = createFileRoute("/_authenticated")({
const nav = [ const nav = [
{ to: "/planes", label: "Planes", icon: GraduationCap }, { to: "/planes", label: "Planes", icon: GraduationCap },
{ to: "/materias", label: "Materias", icon: FileText }, { to: "/asignaturas", label: "Asignaturas", icon: FileText },
{ to: "/dashboard", label: "Dashboard", icon: LayoutDashboard }, { to: "/dashboard", label: "Dashboard", icon: LayoutDashboard },
{ to: "/ajustes", label: "Ajustes", icon: Settings },
] as const ] as const
function getInitials(name?: string) { function getInitials(name?: string) {

View File

@@ -0,0 +1,9 @@
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/_authenticated/asignaturas')({
component: RouteComponent,
})
function RouteComponent() {
return <div>Hello "/_authenticated/asignaturas"!</div>
}

View File

@@ -0,0 +1,9 @@
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/_authenticated/dashboard')({
component: RouteComponent,
})
function RouteComponent() {
return <div>Hello "/_authenticated/dashboard"!</div>
}

View File

@@ -203,7 +203,7 @@ function RouteComponent() {
{recientes.length === 0 && <li className="text-sm text-neutral-500">Sin actividad</li>} {recientes.length === 0 && <li className="text-sm text-neutral-500">Sin actividad</li>}
{recientes.map((r) => ( {recientes.map((r) => (
<li key={`${r.tipo}-${r.id}`} className="flex items-center justify-between gap-3"> <li key={`${r.tipo}-${r.id}`} className="flex items-center justify-between gap-3">
<Link to={`/ _authenticated/${r.tipo === 'plan' ? 'planes' : 'asignaturas'}/${r.id}`} className="truncate hover:underline"> <Link to={`/${r.tipo === 'plan' ? 'planes' : 'asignaturas'}/${r.id}`} className="truncate hover:underline">
<span className="inline-flex items-center gap-2"> <span className="inline-flex items-center gap-2">
{r.tipo === 'plan' ? <Icons.ScrollText className="w-4 h-4" /> : <Icons.BookOpen className="w-4 h-4" />} {r.tipo === 'plan' ? <Icons.ScrollText className="w-4 h-4" /> : <Icons.BookOpen className="w-4 h-4" />}
{r.nombre ?? '—'} {r.nombre ?? '—'}

View File

@@ -1,5 +1,5 @@
import { createFileRoute, useRouter, Link } from "@tanstack/react-router" import { createFileRoute, useRouter, Link } from "@tanstack/react-router"
import { useEffect, useMemo, useState } from "react" import { useMemo, useState } from "react"
import { supabase, useSupabaseAuth } from "@/auth/supabase" import { supabase, useSupabaseAuth } from "@/auth/supabase"
import { Input } from "@/components/ui/input" import { Input } from "@/components/ui/input"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
@@ -7,7 +7,6 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge" import { Badge } from "@/components/ui/badge"
import * as Icons from "lucide-react" import * as Icons from "lucide-react"
import { Plus, RefreshCcw, Building2, ScrollText, BookOpen } from "lucide-react" import { Plus, RefreshCcw, Building2, ScrollText, BookOpen } from "lucide-react"
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"
export type PlanDeEstudios = { export type PlanDeEstudios = {
id: string; nombre: string; nivel: string | null; duracion: string | null; id: string; nombre: string; nivel: string | null; duracion: string | null;
@@ -38,6 +37,7 @@ export const Route = createFileRoute("/_authenticated/planes")({
if (error) throw new Error(error.message) if (error) throw new Error(error.message)
return (data ?? []) as PlanRow[] return (data ?? []) as PlanRow[]
}, },
}) })
/* ---------- helpers de estilo suave ---------- */ /* ---------- helpers de estilo suave ---------- */
@@ -62,10 +62,9 @@ function RouteComponent() {
const [q, setQ] = useState("") const [q, setQ] = useState("")
const data = Route.useLoaderData() as PlanRow[] const data = Route.useLoaderData() as PlanRow[]
const router = useRouter() const router = useRouter()
const search = Route.useSearch<{ planId?: string }>() // usaremos ?planId=... para el modal
const showFacultad = auth.claims?.role === "lci" || auth.claims?.role === "vicerrectoria" const showFacultad = auth.claims?.role === "lci" || auth.claims?.role === "vicerrectoria"
const showCarrera = auth.claims?.role === "secretario_academico" const showCarrera = showFacultad || auth.claims?.role === "secretario_academico"
const filtered = useMemo(() => { const filtered = useMemo(() => {
const term = q.trim().toLowerCase() const term = q.trim().toLowerCase()
@@ -106,11 +105,7 @@ function RouteComponent() {
return ( return (
<Link <Link
key={p.id} key={p.id}
// Runtime navega con ?planId=... (abrimos el modal),
// pero la URL se enmascara SIN el search param:
to="/planes/$planId/modal" to="/planes/$planId/modal"
search={{ planId: p.id }}
mask={{ to: '/planes/$planId', params: { planId: p.id } }}
className="group relative overflow-hidden rounded-3xl bg-white shadow-sm ring-1 transition-all hover:shadow-md hover:-translate-y-0.5" className="group relative overflow-hidden rounded-3xl bg-white shadow-sm ring-1 transition-all hover:shadow-md hover:-translate-y-0.5"
params={{ planId: p.id }} params={{ planId: p.id }}
style={styles} style={styles}
@@ -131,12 +126,12 @@ function RouteComponent() {
<div className="flex items-center gap-2 text-xs"> <div className="flex items-center gap-2 text-xs">
{showCarrera && p.carreras?.nombre && ( {showCarrera && p.carreras?.nombre && (
<Badge variant="secondary" className="border text-neutral-700 bg-white/70"> <Badge variant="secondary" className="border text-neutral-700 bg-white/70 w-fit">
<ScrollText className="mr-1 h-3 w-3" /> {p.carreras?.nombre} <ScrollText className="mr-1 h-3 w-3" /> {p.carreras?.nombre}
</Badge> </Badge>
)} )}
{showFacultad && fac?.nombre && ( {showFacultad && fac?.nombre && (
<Badge variant="outline" className="bg-white/60" style={{ borderColor: styles.borderColor }}> <Badge variant="outline" className="bg-white/60 w-fit" style={{ borderColor: styles.borderColor }}>
<BookOpen className="mr-1 h-3 w-3" /> {fac?.nombre} <BookOpen className="mr-1 h-3 w-3" /> {fac?.nombre}
</Badge> </Badge>
)} )}
@@ -157,95 +152,6 @@ function RouteComponent() {
)} )}
</CardContent> </CardContent>
</Card> </Card>
{/* MODAL: se muestra si existe ?planId=... */}
<PlanPreviewModal planId={search?.planId} onClose={() =>
router.navigate({ to: "/planes", replace: true })
} />
</div> </div>
) )
} }
/* ---------- Modal (carga ligera por id) ---------- */
function PlanPreviewModal({ planId, onClose }: { planId?: string; onClose: () => void }) {
const [loading, setLoading] = useState(false)
const [plan, setPlan] = useState<null | {
id: string; nombre: string; nivel: string | null; duracion: string | null;
total_creditos: number | null; estado: string | null;
carreras: { nombre: string; facultades?: { nombre: string; color?: string | null; icon?: string | null } | null } | null
}>(null)
useEffect(() => {
let alive = true
async function fetchPlan() {
if (!planId) return
setLoading(true)
const { data, error } = await supabase
.from("plan_estudios")
.select(`
id, nombre, nivel, duracion, total_creditos, estado,
carreras (
nombre,
facultades:facultades ( nombre, color, icon )
)
`)
.eq("id", planId)
.single()
if (!alive) return
if (!error) setPlan(data as any)
setLoading(false)
}
fetchPlan()
return () => { alive = false }
}, [planId])
const fac = plan?.carreras?.facultades
const [r, g, b] = hexToRgb(fac?.color)
const headerStyle = { background: `linear-gradient(135deg, rgba(${r},${g},${b},.14), rgba(${r},${g},${b},.06))` }
return (
<Dialog open={!!planId} onOpenChange={() => onClose()}>
<DialogContent className="max-w-2xl p-0 overflow-hidden">
<div className="p-6" style={headerStyle}>
<DialogHeader className="space-y-1">
<DialogTitle>{plan?.nombre ?? "Cargando…"}</DialogTitle>
<div className="text-xs text-neutral-600">
{plan?.carreras?.nombre ?? "—"} {fac?.nombre ? `· ${fac?.nombre}` : ""}
</div>
</DialogHeader>
</div>
<div className="p-6 space-y-4">
{loading && <div className="text-sm text-neutral-500">Cargando</div>}
{!loading && plan && (
<>
<div className="grid grid-cols-2 gap-3 text-sm">
<div><span className="text-neutral-500">Nivel:</span> <span className="font-medium">{plan.nivel ?? "—"}</span></div>
<div><span className="text-neutral-500">Duración:</span> <span className="font-medium">{plan.duracion ?? "—"}</span></div>
<div><span className="text-neutral-500">Créditos:</span> <span className="font-medium">{plan.total_creditos ?? "—"}</span></div>
<div><span className="text-neutral-500">Estado:</span> <span className="font-medium">{plan.estado ?? "—"}</span></div>
</div>
<div className="flex gap-2">
<Link
to="/_authenticated/planes/$planId"
params={{ planId: plan.id }}
className="inline-flex items-center gap-2 rounded-xl bg-black text-white px-4 py-2 hover:opacity-90"
>
<Icons.FileText className="w-4 h-4" /> Ver ficha
</Link>
<Link
to="/_authenticated/asignaturas"
search={{ planId: plan.id }}
className="inline-flex items-center gap-2 rounded-xl border px-4 py-2 hover:bg-neutral-50"
>
<Icons.BookOpen className="w-4 h-4" /> Ver asignaturas
</Link>
</div>
</>
)}
</div>
</DialogContent>
</Dialog>
)
}

View File

@@ -33,7 +33,7 @@ export const Route = createFileRoute("/_authenticated/planes/$planId/modal")({
.eq("id", params.planId) .eq("id", params.planId)
.single() .single()
if (error) throw error if (error) throw error
return data as PlanDetail return data
}, },
}) })