refactor: update navigation links and search parameters across authenticated routes
This commit is contained in:
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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,8 +346,7 @@ 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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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" />
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user