refactor: update navigation links and search parameters across authenticated routes

This commit is contained in:
2025-08-29 16:51:22 -06:00
parent f8de39e6d1
commit 6c3dd54d5f
6 changed files with 41 additions and 26 deletions

View File

@@ -102,7 +102,7 @@ function Layout() {
</Sheet> </Sheet>
{/* Brand */} {/* Brand */}
<Link to={user.isAdmin ? "/dashboard" : "/planes"} className="hidden items-center gap-2 md:flex"> <Link to="/dashboard" className="hidden items-center gap-2 md:flex">
<span className="inline-flex h-8 w-25 items-center justify-center rounded-xl bg-primary/10 text-primary font-bold"> <span className="inline-flex h-8 w-25 items-center justify-center rounded-xl bg-primary/10 text-primary font-bold">
<img src="https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcS8C2SvZe281wTNc9werkudO9aJiX3dOZm9T3s5DAfS0OUOvDbgc_WC61U_esY8GE8bZoI&usqp=CAU" alt="" /> <img src="https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcS8C2SvZe281wTNc9werkudO9aJiX3dOZm9T3s5DAfS0OUOvDbgc_WC61U_esY8GE8bZoI&usqp=CAU" alt="" />
</span> </span>
@@ -165,6 +165,7 @@ function Sidebar({ onNavigate }: { onNavigate?: () => void }) {
<Link <Link
key={item.to} key={item.to}
to={item.to} to={item.to}
search={{ ...(item.to === '/planes' ? { plan: '' } : {}) }}
activeOptions={{ exact: true }} activeOptions={{ exact: true }}
activeProps={{ className: "bg-primary/10 text-foreground" }} activeProps={{ className: "bg-primary/10 text-foreground" }}
className="group inline-flex items-center gap-3 rounded-xl px-3 py-2 text-sm text-muted-foreground hover:bg-primary/10 hover:text-foreground" className="group inline-flex items-center gap-3 rounded-xl px-3 py-2 text-sm text-muted-foreground hover:bg-primary/10 hover:text-foreground"

View File

@@ -53,7 +53,7 @@ const planesKeys = {
all: () => [...planesKeys.root, 'all'] as const, all: () => [...planesKeys.root, 'all'] as const,
} }
async function fetchPlanIdsByScope(search: Pick<SearchState, 'planId'|'carreraId'|'facultadId'>): Promise<string[] | null> { async function fetchPlanIdsByScope(search: Pick<SearchState, 'planId' | 'carreraId' | 'facultadId'>): Promise<string[] | null> {
const { planId, carreraId, facultadId } = search const { planId, carreraId, facultadId } = search
if (planId) return [planId] if (planId) return [planId]
if (carreraId) { if (carreraId) {
@@ -369,7 +369,7 @@ function RouteComponent() {
<Link <Link
to="/planes" to="/planes"
className="hidden sm:inline-flex items-center gap-2 rounded-xl border px-3 py-2 text-sm hover:bg-neutral-50" className="hidden sm:inline-flex items-center gap-2 rounded-xl border px-3 py-2 text-sm hover:bg-neutral-50"
search={{ crear: 'asignatura' }} search={{ plan: '' }}
> >
<Icons.Plus className="w-4 h-4" /> Nueva asignatura <Icons.Plus className="w-4 h-4" /> Nueva asignatura
</Link> </Link>

View File

@@ -1,4 +1,4 @@
import { createFileRoute, Link, useRouter } from '@tanstack/react-router' import { createFileRoute, Link, useNavigate, useRouter } from '@tanstack/react-router'
import { useSuspenseQuery, queryOptions, useQueryClient } from '@tanstack/react-query' import { useSuspenseQuery, queryOptions, useQueryClient } from '@tanstack/react-query'
import { supabase, useSupabaseAuth } from '@/auth/supabase' import { supabase, useSupabaseAuth } from '@/auth/supabase'
import * as Icons from 'lucide-react' import * as Icons from 'lucide-react'
@@ -178,6 +178,8 @@ function RouteComponent() {
const isAdmin = !!auth.claims?.claims_admin const isAdmin = !!auth.claims?.claims_admin
const role = auth.claims?.role as 'lci' | 'vicerrectoria' | 'secretario_academico' | 'jefe_carrera' | 'planeacion' | undefined const role = auth.claims?.role as 'lci' | 'vicerrectoria' | 'secretario_academico' | 'jefe_carrera' | 'planeacion' | undefined
const navigate = useNavigate({ from: Route.fullPath })
// Mensaje contextual // Mensaje contextual
const roleHint = useMemo(() => { const roleHint = useMemo(() => {
switch (role) { switch (role) {
@@ -227,7 +229,7 @@ function RouteComponent() {
if (e.key === 'Enter') { if (e.key === 'Enter') {
const q = (e.target as HTMLInputElement).value.trim() const q = (e.target as HTMLInputElement).value.trim()
if (!q) return if (!q) return
router.navigate({ to: '/planes', search: { q } }) navigate({ to: '/planes', search: { plan: q } })
} }
}} }}
/> />
@@ -246,7 +248,7 @@ function RouteComponent() {
{/* Atajos rápidos (según rol) */} {/* Atajos rápidos (según rol) */}
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
<Link to="/planes" className="inline-flex items-center gap-2 rounded-xl bg-white/20 border border-white/30 px-3 py-1.5 text-sm hover:bg-white/30"> <Link to="/planes" search={{ plan: '' }} className="inline-flex items-center gap-2 rounded-xl bg-white/20 border border-white/30 px-3 py-1.5 text-sm hover:bg-white/30">
<Icons.ScrollText className="w-4 h-4" /> Nuevo plan <Icons.ScrollText className="w-4 h-4" /> Nuevo plan
</Link> </Link>
<Link <Link
@@ -267,10 +269,10 @@ function RouteComponent() {
{/* KPIs principales */} {/* KPIs principales */}
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4"> <div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
<Tile to="/_authenticated/facultades" label="Facultades" value={kpis.facultades} Icon={Icons.Building2} /> <Tile to="/facultades" label="Facultades" value={kpis.facultades} Icon={Icons.Building2} />
<Tile to="/_authenticated/carreras" label="Carreras" value={kpis.carreras} Icon={Icons.GraduationCap} /> <Tile to="/carreras" label="Carreras" value={kpis.carreras} Icon={Icons.GraduationCap} />
<Tile to="/_authenticated/planes" label="Planes de estudio" value={kpis.planes} Icon={Icons.ScrollText} /> <Tile to="/planes" label="Planes de estudio" value={kpis.planes} Icon={Icons.ScrollText} />
<Tile to="/_authenticated/asignaturas" label="Asignaturas" value={kpis.asignaturas} Icon={Icons.BookOpen} /> <Tile to="/asignaturas" label="Asignaturas" value={kpis.asignaturas} Icon={Icons.BookOpen} />
</div> </div>
{/* Calidad + Salud */} {/* Calidad + Salud */}
@@ -344,9 +346,8 @@ function HealthRow({ label, value, to }: { label: string; value: number; to: str
return ( return (
<Link <Link
to={to} to={to}
className={`flex items-center justify-between rounded-xl px-4 py-3 ring-1 ${ className={`flex items-center justify-between rounded-xl px-4 py-3 ring-1 ${warn ? 'ring-amber-300 bg-amber-50 text-amber-800 hover:bg-amber-100' : 'ring-neutral-200 hover:bg-neutral-50'
warn ? 'ring-amber-300 bg-amber-50 text-amber-800 hover:bg-amber-100' : 'ring-neutral-200 hover:bg-neutral-50' } transition-colors`}
} transition-colors`}
> >
<span className="text-sm">{label}</span> <span className="text-sm">{label}</span>
<span className="text-lg font-semibold tabular-nums">{value}</span> <span className="text-lg font-semibold tabular-nums">{value}</span>

View File

@@ -65,7 +65,7 @@ function RouteComponent() {
<div className="relative p-6 space-y-6"> <div className="relative p-6 space-y-6">
<GradientMesh color={fac?.color} /> <GradientMesh color={fac?.color} />
<nav className="relative text-sm text-neutral-500"> <nav className="relative text-sm text-neutral-500">
<Link to="/planes" className="hover:underline">Planes de estudio</Link> <Link to="/planes" search={{ plan: '' }} className="hover:underline">Planes de estudio</Link>
<span className="mx-1">/</span> <span className="mx-1">/</span>
<span className="text-primary">{plan.nombre}</span> <span className="text-primary">{plan.nombre}</span>
</nav> </nav>
@@ -345,10 +345,10 @@ function AsignaturaPreviewCard({ asignatura }: { asignatura: AsignaturaLite }) {
const tipo = (extra?.tipo ?? "").toLowerCase() const tipo = (extra?.tipo ?? "").toLowerCase()
const tipoChip = const tipoChip =
tipo.includes("oblig") ? "bg-emerald-50 text-emerald-700 border-emerald-200" : tipo.includes("oblig") ? "bg-emerald-50 text-emerald-700 border-emerald-200" :
tipo.includes("opt") ? "bg-amber-50 text-amber-800 border-amber-200" : tipo.includes("opt") ? "bg-amber-50 text-amber-800 border-amber-200" :
tipo.includes("taller") ? "bg-indigo-50 text-indigo-700 border-indigo-200" : tipo.includes("taller") ? "bg-indigo-50 text-indigo-700 border-indigo-200" :
tipo.includes("lab") ? "bg-sky-50 text-sky-700 border-sky-200" : tipo.includes("lab") ? "bg-sky-50 text-sky-700 border-sky-200" :
"bg-neutral-100 text-neutral-700 border-neutral-200" "bg-neutral-100 text-neutral-700 border-neutral-200"
return ( return (
<article className="group relative overflow-hidden rounded-2xl border bg-white/70 dark:bg-neutral-900/60 backdrop-blur p-4 shadow-sm hover:shadow-md transition-all hover:-translate-y-0.5" role="region" aria-label={asignatura.nombre}> <article className="group relative overflow-hidden rounded-2xl border bg-white/70 dark:bg-neutral-900/60 backdrop-blur p-4 shadow-sm hover:shadow-md transition-all hover:-translate-y-0.5" role="region" aria-label={asignatura.nombre}>

View File

@@ -1,4 +1,4 @@
import { createFileRoute, useRouter, Link } from "@tanstack/react-router" import { createFileRoute, useRouter, Link, useNavigate } from "@tanstack/react-router"
import { 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"
@@ -10,6 +10,8 @@ import { Plus, RefreshCcw, Building2 } from "lucide-react"
import { InfoChip } from "@/components/planes/InfoChip" import { InfoChip } from "@/components/planes/InfoChip"
import { CreatePlanDialog } from "@/components/planes/CreatePlanDialog" import { CreatePlanDialog } from "@/components/planes/CreatePlanDialog"
import { chipTint } from "@/components/planes/chipTint" import { chipTint } from "@/components/planes/chipTint"
import { z } from 'zod'
export type PlanDeEstudios = { export type PlanDeEstudios = {
@@ -23,6 +25,11 @@ type PlanRow = PlanDeEstudios & {
} | null } | null
} }
const planSearchSchema = z.object({
plan: z.string().nullable()
})
export const Route = createFileRoute("/_authenticated/planes")({ export const Route = createFileRoute("/_authenticated/planes")({
component: RouteComponent, component: RouteComponent,
loader: async () => { loader: async () => {
@@ -41,29 +48,31 @@ 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[]
}, },
validateSearch: planSearchSchema,
}) })
function RouteComponent() { function RouteComponent() {
const auth = useSupabaseAuth() const auth = useSupabaseAuth()
const [q, setQ] = useState("") const { plan } = Route.useSearch()
const [openCreate, setOpenCreate] = useState(false) const [openCreate, setOpenCreate] = useState(false)
const data = Route.useLoaderData() as PlanRow[] const data = Route.useLoaderData() as PlanRow[]
const router = useRouter() const router = useRouter()
const navigate = useNavigate({ from: Route.fullPath })
const showFacultad = auth.claims?.role === "lci" || auth.claims?.role === "vicerrectoria" const showFacultad = auth.claims?.role === "lci" || auth.claims?.role === "vicerrectoria"
const showCarrera = showFacultad || 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 = plan?.trim().toLowerCase()
if (!term || !data) return data if (!term || !data) return data
return data.filter((p) => return data.filter((p) =>
[p.nombre, p.nivel, p.estado, p.carreras?.nombre, p.carreras?.facultades?.nombre] [p.nombre, p.nivel, p.estado, p.carreras?.nombre, p.carreras?.facultades?.nombre]
.filter(Boolean) .filter(Boolean)
.some((v) => String(v).toLowerCase().includes(term)) .some((v) => String(v).toLowerCase().includes(term))
) )
}, [q, data]) }, [plan, data])
return ( return (
<div className="space-y-4"> <div className="space-y-4">
@@ -72,7 +81,11 @@ function RouteComponent() {
<CardTitle className="text-xl">Planes de estudio</CardTitle> <CardTitle className="text-xl">Planes de estudio</CardTitle>
<div className="flex w-full items-center gap-2 md:w-auto"> <div className="flex w-full items-center gap-2 md:w-auto">
<div className="relative w-full md:w-80"> <div className="relative w-full md:w-80">
<Input value={q} onChange={(e) => setQ(e.target.value)} placeholder="Buscar por nombre, nivel, estado…" /> <Input
value={plan ?? ''}
onChange={e => navigate({ search: { plan: e.target.value } })}
placeholder="Buscar por nombre, nivel, estado…"
/>
</div> </div>
<Button variant="outline" size="icon" onClick={() => router.invalidate()} title="Recargar"> <Button variant="outline" size="icon" onClick={() => router.invalidate()} title="Recargar">
<RefreshCcw className="h-4 w-4" /> <RefreshCcw className="h-4 w-4" />

View File

@@ -15,7 +15,7 @@ function App() {
<header className="flex items-center justify-between px-10 py-6 border-b border-slate-700/50"> <header className="flex items-center justify-between px-10 py-6 border-b border-slate-700/50">
<h1 className="text-2xl font-mono tracking-tight">Acad-IA</h1> <h1 className="text-2xl font-mono tracking-tight">Acad-IA</h1>
{isAuth ? ( {isAuth ? (
<Link to="/planes"> <Link to="/dashboard">
<Button className="border-slate-500 hover:bg-slate-700/50 relative overflow-hidden"> <Button className="border-slate-500 hover:bg-slate-700/50 relative overflow-hidden">
<span className="-z-1 absolute inset-0 animate-aurora" /> <span className="-z-1 absolute inset-0 animate-aurora" />
@@ -23,7 +23,7 @@ function App() {
</Button> </Button>
</Link> </Link>
) : ( ) : (
<Link to="/login" search={{ redirect: '/planes' }}> <Link to="/login" search={{ redirect: '/dashboard' }}>
<Button variant="outline" className="border-slate-500 hover:bg-slate-700/50"> <Button variant="outline" className="border-slate-500 hover:bg-slate-700/50">
Iniciar sesión Iniciar sesión
</Button> </Button>