refactor: update navigation links and search parameters across authenticated routes
This commit is contained in:
@@ -53,7 +53,7 @@ const planesKeys = {
|
||||
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
|
||||
if (planId) return [planId]
|
||||
if (carreraId) {
|
||||
@@ -369,7 +369,7 @@ function RouteComponent() {
|
||||
<Link
|
||||
to="/planes"
|
||||
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
|
||||
</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 { supabase, useSupabaseAuth } from '@/auth/supabase'
|
||||
import * as Icons from 'lucide-react'
|
||||
@@ -178,6 +178,8 @@ function RouteComponent() {
|
||||
const isAdmin = !!auth.claims?.claims_admin
|
||||
const role = auth.claims?.role as 'lci' | 'vicerrectoria' | 'secretario_academico' | 'jefe_carrera' | 'planeacion' | undefined
|
||||
|
||||
const navigate = useNavigate({ from: Route.fullPath })
|
||||
|
||||
// Mensaje contextual
|
||||
const roleHint = useMemo(() => {
|
||||
switch (role) {
|
||||
@@ -227,7 +229,7 @@ function RouteComponent() {
|
||||
if (e.key === 'Enter') {
|
||||
const q = (e.target as HTMLInputElement).value.trim()
|
||||
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) */}
|
||||
<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
|
||||
</Link>
|
||||
<Link
|
||||
@@ -267,10 +269,10 @@ function RouteComponent() {
|
||||
|
||||
{/* KPIs principales */}
|
||||
<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="/_authenticated/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="/_authenticated/asignaturas" label="Asignaturas" value={kpis.asignaturas} Icon={Icons.BookOpen} />
|
||||
<Tile to="/facultades" label="Facultades" value={kpis.facultades} Icon={Icons.Building2} />
|
||||
<Tile to="/carreras" label="Carreras" value={kpis.carreras} Icon={Icons.GraduationCap} />
|
||||
<Tile to="/planes" label="Planes de estudio" value={kpis.planes} Icon={Icons.ScrollText} />
|
||||
<Tile to="/asignaturas" label="Asignaturas" value={kpis.asignaturas} Icon={Icons.BookOpen} />
|
||||
</div>
|
||||
|
||||
{/* Calidad + Salud */}
|
||||
@@ -344,9 +346,8 @@ function HealthRow({ label, value, to }: { label: string; value: number; to: str
|
||||
return (
|
||||
<Link
|
||||
to={to}
|
||||
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'
|
||||
} transition-colors`}
|
||||
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'
|
||||
} transition-colors`}
|
||||
>
|
||||
<span className="text-sm">{label}</span>
|
||||
<span className="text-lg font-semibold tabular-nums">{value}</span>
|
||||
|
||||
@@ -65,7 +65,7 @@ function RouteComponent() {
|
||||
<div className="relative p-6 space-y-6">
|
||||
<GradientMesh color={fac?.color} />
|
||||
<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="text-primary">{plan.nombre}</span>
|
||||
</nav>
|
||||
@@ -345,10 +345,10 @@ function AsignaturaPreviewCard({ asignatura }: { asignatura: AsignaturaLite }) {
|
||||
const tipo = (extra?.tipo ?? "").toLowerCase()
|
||||
const tipoChip =
|
||||
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("taller") ? "bg-indigo-50 text-indigo-700 border-indigo-200" :
|
||||
tipo.includes("lab") ? "bg-sky-50 text-sky-700 border-sky-200" :
|
||||
"bg-neutral-100 text-neutral-700 border-neutral-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("lab") ? "bg-sky-50 text-sky-700 border-sky-200" :
|
||||
"bg-neutral-100 text-neutral-700 border-neutral-200"
|
||||
|
||||
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}>
|
||||
|
||||
@@ -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 { supabase, useSupabaseAuth } from "@/auth/supabase"
|
||||
import { Input } from "@/components/ui/input"
|
||||
@@ -10,6 +10,8 @@ import { Plus, RefreshCcw, Building2 } from "lucide-react"
|
||||
import { InfoChip } from "@/components/planes/InfoChip"
|
||||
import { CreatePlanDialog } from "@/components/planes/CreatePlanDialog"
|
||||
import { chipTint } from "@/components/planes/chipTint"
|
||||
import { z } from 'zod'
|
||||
|
||||
|
||||
|
||||
export type PlanDeEstudios = {
|
||||
@@ -23,6 +25,11 @@ type PlanRow = PlanDeEstudios & {
|
||||
} | null
|
||||
}
|
||||
|
||||
const planSearchSchema = z.object({
|
||||
plan: z.string().nullable()
|
||||
})
|
||||
|
||||
|
||||
export const Route = createFileRoute("/_authenticated/planes")({
|
||||
component: RouteComponent,
|
||||
loader: async () => {
|
||||
@@ -41,29 +48,31 @@ export const Route = createFileRoute("/_authenticated/planes")({
|
||||
if (error) throw new Error(error.message)
|
||||
return (data ?? []) as PlanRow[]
|
||||
},
|
||||
|
||||
validateSearch: planSearchSchema,
|
||||
})
|
||||
|
||||
|
||||
function RouteComponent() {
|
||||
const auth = useSupabaseAuth()
|
||||
const [q, setQ] = useState("")
|
||||
const { plan } = 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 filtered = useMemo(() => {
|
||||
const term = q.trim().toLowerCase()
|
||||
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))
|
||||
)
|
||||
}, [q, data])
|
||||
}, [plan, data])
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
@@ -72,7 +81,11 @@ function RouteComponent() {
|
||||
<CardTitle className="text-xl">Planes de estudio</CardTitle>
|
||||
<div className="flex w-full items-center gap-2 md:w-auto">
|
||||
<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>
|
||||
<Button variant="outline" size="icon" onClick={() => router.invalidate()} title="Recargar">
|
||||
<RefreshCcw className="h-4 w-4" />
|
||||
|
||||
Reference in New Issue
Block a user