diff --git a/src/components/carreras/CarreraDetailDialog.tsx b/src/components/carreras/CarreraDetailDialog.tsx new file mode 100644 index 0000000..ab3cd5d --- /dev/null +++ b/src/components/carreras/CarreraDetailDialog.tsx @@ -0,0 +1,170 @@ +import { useMemo, useState } from "react" +import { useQuery, useQueryClient } from "@tanstack/react-query" +import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog" +import { Input } from "@/components/ui/input" +import { Button } from "@/components/ui/button" +import { Badge } from "@/components/ui/badge" +import { Accordion, AccordionItem, AccordionTrigger, AccordionContent } from "@/components/ui/accordion" +import * as Icons from "lucide-react" +import { type CarreraRow } from "@/routes/_authenticated/carreras" +import { criteriosOptions } from "@/routes/_authenticated/carreras" +import { CriterioFormDialog } from "./CriterioFormDialog" +import { supabase } from "@/auth/supabase" + +export function CarreraDetailDialog({ + carrera, + onOpenChange, + onChanged, +}: { + carrera: CarreraRow | null + onOpenChange: (c: CarreraRow | null) => void + onChanged?: () => void +}) { + const carreraId = carrera?.id ?? "" + const { data: criterios = [], isFetching } = useQuery({ + ...criteriosOptions(carreraId || "noop"), + enabled: !!carreraId, + }) + const qc = useQueryClient() + const [q, setQ] = useState("") + const [newCritOpen, setNewCritOpen] = useState(false) + const [deletingId, setDeletingId] = useState(null) + + const filtered = useMemo(() => { + const t = q.trim().toLowerCase() + if (!t) return criterios + return criterios.filter((c) => + [c.nombre, c.descripcion, c.tipo, c.referencia_documento] + .filter(Boolean) + .some((v) => String(v).toLowerCase().includes(t)) + ) + }, [q, criterios]) + + async function removeCriterio(id: number) { + if (!carreraId) return + if (!confirm("¿Seguro que quieres eliminar este criterio?")) return + setDeletingId(id) + const { error } = await supabase.from("criterios_carrera").delete().eq("id", id) + setDeletingId(null) + if (error) { + alert(error.message) + return + } + await qc.invalidateQueries({ queryKey: criteriosOptions(carreraId).queryKey }) + onChanged?.() + } + + return ( + !o && onOpenChange(null)}> + + + {carrera?.nombre} + + {carrera?.facultades?.nombre ?? "—"} · {carrera?.semestres} semestres{" "} + {typeof carrera?.activo === "boolean" && ( + + {carrera?.activo ? "Activa" : "Inactiva"} + + )} + + + +
+
+
+ + setQ(e.target.value)} + placeholder="Buscar criterio por nombre, tipo o referencia…" + className="pl-8" + /> +
+ +
+ + {isFetching ? ( +
Cargando criterios…
+ ) : ( + <> +
+ {filtered.length} criterio(s) + {q ? " (filtrado)" : ""} +
+ + {filtered.length === 0 ? ( +
No hay criterios
+ ) : ( + + {filtered.map((c) => ( + + +
+ {c.nombre} +
+ {c.tipo && {c.tipo}} + {c.obligatorio ? "Obligatorio" : "Opcional"} + +
+
+
+ + {c.descripcion &&

{c.descripcion}

} +
+ {c.referencia_documento && ( + + + + Referencia + + + )} + {c.fecha_creacion && ( + + + {new Date(c.fecha_creacion).toLocaleString()} + + )} +
+
+
+ ))} +
+ )} + + )} +
+ + + + + + {/* Crear criterio */} + +
+
+ ) +} diff --git a/src/components/carreras/CarreraFormDialog.tsx b/src/components/carreras/CarreraFormDialog.tsx new file mode 100644 index 0000000..b92a8b3 --- /dev/null +++ b/src/components/carreras/CarreraFormDialog.tsx @@ -0,0 +1,133 @@ +import { useEffect, useState } from "react" +import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog" +import { Input } from "@/components/ui/input" +import { Button } from "@/components/ui/button" +import { Label } from "@/components/ui/label" +import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "@/components/ui/select" +import { Switch } from "@/components/ui/switch" +import { supabase } from "@/auth/supabase" +import { type CarreraRow, type FacultadLite } from "@/routes/_authenticated/carreras" + +export function CarreraFormDialog({ + open, + onOpenChange, + mode, + carrera, + facultades, + onSaved, +}: { + open: boolean + onOpenChange: (o: boolean) => void + mode: "create" | "edit" + carrera?: CarreraRow + facultades: FacultadLite[] + onSaved?: () => void +}) { + const [saving, setSaving] = useState(false) + const [nombre, setNombre] = useState(carrera?.nombre ?? "") + const [semestres, setSemestres] = useState(carrera?.semestres ?? 9) + const [activo, setActivo] = useState(carrera?.activo ?? true) + const [facultadId, setFacultadId] = useState(carrera?.facultad_id ?? "none") + + useEffect(() => { + if (mode === "edit" && carrera) { + setNombre(carrera.nombre) + setSemestres(carrera.semestres) + setActivo(carrera.activo) + setFacultadId(carrera.facultad_id ?? "none") + } else if (mode === "create") { + setNombre("") + setSemestres(9) + setActivo(true) + setFacultadId("none") + } + }, [mode, carrera, open]) + + async function save() { + if (!nombre.trim()) { + alert("Escribe un nombre") + return + } + setSaving(true) + const payload = { + nombre: nombre.trim(), + semestres: Number(semestres) || 9, + activo, + facultad_id: facultadId === "none" ? null : facultadId, + } + + const action = + mode === "create" + ? supabase.from("carreras").insert([payload]).select("id").single() + : supabase.from("carreras").update(payload).eq("id", carrera!.id).select("id").single() + + const { error } = await action + setSaving(false) + if (error) { + alert(error.message) + return + } + onOpenChange(false) + onSaved?.() + } + + return ( + + + + {mode === "create" ? "Nueva carrera" : "Editar carrera"} + + {mode === "create" ? "Crea una nueva carrera en la base de datos." : "Actualiza los datos de la carrera."} + + + +
+
+ + setNombre(e.target.value)} placeholder="Ing. en Software" /> +
+ +
+
+ + setSemestres(parseInt(e.target.value || "9", 10))} /> +
+
+ +
+ + {activo ? "Activa" : "Inactiva"} +
+
+
+ +
+ + +
+
+ + + + + +
+
+ ) +} diff --git a/src/components/carreras/CriterioFormDialog.tsx b/src/components/carreras/CriterioFormDialog.tsx new file mode 100644 index 0000000..814b472 --- /dev/null +++ b/src/components/carreras/CriterioFormDialog.tsx @@ -0,0 +1,114 @@ +import { useEffect, useState } from "react" +import { useQueryClient } from "@tanstack/react-query" +import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog" +import { Input } from "@/components/ui/input" +import { Button } from "@/components/ui/button" +import { Label } from "@/components/ui/label" +import { Switch } from "@/components/ui/switch" +import { supabase } from "@/auth/supabase" +import { criteriosKeys } from "@/routes/_authenticated/carreras" + +export function CriterioFormDialog({ + open, + onOpenChange, + carreraId, + onSaved, +}: { + open: boolean + onOpenChange: (o: boolean) => void + carreraId: string + onSaved?: () => void +}) { + const qc = useQueryClient() + const [saving, setSaving] = useState(false) + const [nombre, setNombre] = useState("") + const [tipo, setTipo] = useState("") + const [descripcion, setDescripcion] = useState("") + const [obligatorio, setObligatorio] = useState(true) + const [referencia, setReferencia] = useState("") + + useEffect(() => { + if (!open) { + setNombre("") + setTipo("") + setDescripcion("") + setObligatorio(true) + setReferencia("") + } + }, [open]) + + async function save() { + if (!carreraId) return + if (!nombre.trim()) { + alert("Escribe un nombre") + return + } + setSaving(true) + const { error } = await supabase.from("criterios_carrera").insert([ + { + nombre: nombre.trim(), + tipo: tipo || null, + descripcion: descripcion || null, + obligatorio, + referencia_documento: referencia || null, + carrera_id: carreraId, + }, + ]) + setSaving(false) + if (error) { + alert(error.message) + return + } + onOpenChange(false) + await qc.invalidateQueries({ queryKey: criteriosKeys.byCarrera(carreraId) }) + onSaved?.() + } + + return ( + + + + Nuevo criterio + Agrega un criterio para esta carrera. + + +
+
+ + setNombre(e.target.value)} placeholder="Infraestructura de laboratorios" /> +
+
+ + setTipo(e.target.value)} placeholder="Académico / Operativo / Otro" /> +
+
+ + setDescripcion(e.target.value)} placeholder="Detalle o alcance del criterio" /> +
+
+
+ +
+ + {obligatorio ? "Sí" : "No"} +
+
+
+ + setReferencia(e.target.value)} placeholder="https://…" /> +
+
+
+ + + + + +
+
+ ) +} diff --git a/src/components/carreras/StatusPill.tsx b/src/components/carreras/StatusPill.tsx new file mode 100644 index 0000000..36d6eac --- /dev/null +++ b/src/components/carreras/StatusPill.tsx @@ -0,0 +1,12 @@ +export function StatusPill({ active }: { active: boolean }) { + return ( + + {active ? "Activa" : "Inactiva"} + + ) +} diff --git a/src/components/carreras/openContextMenu.ts b/src/components/carreras/openContextMenu.ts new file mode 100644 index 0000000..3874d81 --- /dev/null +++ b/src/components/carreras/openContextMenu.ts @@ -0,0 +1,15 @@ +export function openContextMenu(e: React.MouseEvent) { + e.preventDefault() + e.stopPropagation() + // Simulate right click by opening context menu + const trigger = e.currentTarget + if (!(trigger instanceof HTMLElement)) return + const event = new window.MouseEvent("contextmenu", { + bubbles: true, + cancelable: true, + view: window, + clientX: e.clientX, + clientY: e.clientY, + }) + trigger.dispatchEvent(event) +} diff --git a/src/components/carreras/utils.ts b/src/components/carreras/utils.ts new file mode 100644 index 0000000..4db19f0 --- /dev/null +++ b/src/components/carreras/utils.ts @@ -0,0 +1,10 @@ +export const tint = (hex?: string | null, a = 0.18) => { + if (!hex) return `rgba(37,99,235,${a})` + const h = hex.replace("#", "") + const v = h.length === 3 ? h.split("").map((c) => c + c).join("") : h + const n = parseInt(v, 16) + const r = (n >> 16) & 255, + g = (n >> 8) & 255, + b = n & 255 + return `rgba(${r},${g},${b},${a})` +} diff --git a/src/routes/_authenticated/archivos.tsx b/src/routes/_authenticated/archivos.tsx index 1dcf901..825e782 100644 --- a/src/routes/_authenticated/archivos.tsx +++ b/src/routes/_authenticated/archivos.tsx @@ -90,7 +90,7 @@ function RouteComponent() {
- Archivos de referencia + Archivos de referencia
diff --git a/src/routes/_authenticated/asignaturas.tsx b/src/routes/_authenticated/asignaturas.tsx index 859cbc7..81d4c00 100644 --- a/src/routes/_authenticated/asignaturas.tsx +++ b/src/routes/_authenticated/asignaturas.tsx @@ -361,7 +361,7 @@ function RouteComponent() {
-

+

Asignaturas

@@ -705,7 +705,7 @@ function AsignaturaCard({ a, onClone, onAddToCart }: { a: Asignatura; onClone: (
-

{a.nombre}

+

{a.nombre}

diff --git a/src/routes/_authenticated/carreras.tsx b/src/routes/_authenticated/carreras.tsx index c07c8b6..71a602c 100644 --- a/src/routes/_authenticated/carreras.tsx +++ b/src/routes/_authenticated/carreras.tsx @@ -1,24 +1,29 @@ // routes/_authenticated/carreras.tsx (refactor a TanStack Query v5) import { createFileRoute, useRouter } from "@tanstack/react-router" -import { useEffect, useMemo, useState } from "react" -import { useSuspenseQuery, useQueryClient, queryOptions, useQuery } from "@tanstack/react-query" +import { useMemo, useState } from "react" +import { useSuspenseQuery, useQueryClient, queryOptions } from "@tanstack/react-query" import { supabase } from "@/auth/supabase" import * as Icons from "lucide-react" - import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { Input } from "@/components/ui/input" import { Button } from "@/components/ui/button" -import { Badge } from "@/components/ui/badge" -import { Label } from "@/components/ui/label" -import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog" +// import { Badge } from "@/components/ui/badge" // unused +// import { Label } from "@/components/ui/label" // unused +// import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog" // unused import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "@/components/ui/select" -import { Accordion, AccordionItem, AccordionTrigger, AccordionContent } from "@/components/ui/accordion" -import { Switch } from "@/components/ui/switch" +// import { Accordion, AccordionItem, AccordionTrigger, AccordionContent } from "@/components/ui/accordion" // unused +// import { Switch } from "@/components/ui/switch" // unused import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuTrigger } from "@/components/ui/context-menu" import { useDeleteCarreraDialog } from "@/components/carreras/DeleteCarreras" +// Modularized components +import { CarreraFormDialog } from "@/components/carreras/CarreraFormDialog" +import { CarreraDetailDialog } from "@/components/carreras/CarreraDetailDialog" +import { StatusPill } from "@/components/carreras/StatusPill" +import { tint } from "@/components/carreras/utils" +import { openContextMenu } from "@/components/carreras/openContextMenu" /* -------------------- Tipos -------------------- */ -type FacultadLite = { id: string; nombre: string; color?: string | null; icon?: string | null } +export type FacultadLite = { id: string; nombre: string; color?: string | null; icon?: string | null } export type CarreraRow = { id: string nombre: string @@ -33,11 +38,11 @@ export const carrerasKeys = { root: ["carreras"] as const, list: () => [...carrerasKeys.root, "list"] as const, } -const facultadesKeys = { +export const facultadesKeys = { root: ["facultades"] as const, all: () => [...facultadesKeys.root, "all"] as const, } -const criteriosKeys = { +export const criteriosKeys = { root: ["criterios_carrera"] as const, byCarrera: (id: string) => [...criteriosKeys.root, { carreraId: id }] as const, } @@ -82,13 +87,13 @@ async function fetchCriterios(carreraId: string): Promise { return (data ?? []) as CriterioRow[] } -const carrerasOptions = () => +export const carrerasOptions = () => queryOptions({ queryKey: carrerasKeys.list(), queryFn: fetchCarreras, staleTime: 60_000 }) -const facultadesOptions = () => +export const facultadesOptions = () => queryOptions({ queryKey: facultadesKeys.all(), queryFn: fetchFacultades, staleTime: 5 * 60_000 }) -const criteriosOptions = (carreraId: string) => +export const criteriosOptions = (carreraId: string) => queryOptions({ queryKey: criteriosKeys.byCarrera(carreraId), queryFn: () => fetchCriterios(carreraId) }) /* -------------------- Ruta -------------------- */ @@ -104,27 +109,7 @@ export const Route = createFileRoute("/_authenticated/carreras")({ }, }) -/* -------------------- Helpers UI -------------------- */ -const tint = (hex?: string | null, a = 0.18) => { - if (!hex) return `rgba(37,99,235,${a})` - const h = hex.replace("#", "") - const v = h.length === 3 ? h.split("").map((c) => c + c).join("") : h - const n = parseInt(v, 16) - const r = (n >> 16) & 255, - g = (n >> 8) & 255, - b = n & 255 - return `rgba(${r},${g},${b},${a})` -} -const StatusPill = ({ active }: { active: boolean }) => ( - - {active ? "Activa" : "Inactiva"} - -) +// ...existing code... /* -------------------- Página -------------------- */ function RouteComponent() { @@ -157,7 +142,7 @@ function RouteComponent() {
- Carreras + Carreras
@@ -299,364 +284,8 @@ function RouteComponent() { ) } -function openContextMenu(e: React.MouseEvent) { - e.preventDefault() - e.stopPropagation() - // Simulate right click by opening context menu - const trigger = e.currentTarget - if (!(trigger instanceof HTMLElement)) return - const event = new window.MouseEvent("contextmenu", { - bubbles: true, - cancelable: true, - view: window, - clientX: e.clientX, - clientY: e.clientY, - }) - trigger.dispatchEvent(event) -} +// ...existing code... -/* -------------------- Form crear/editar -------------------- */ -function CarreraFormDialog({ - open, - onOpenChange, - mode, - carrera, - facultades, - onSaved, -}: { - open: boolean - onOpenChange: (o: boolean) => void - mode: "create" | "edit" - carrera?: CarreraRow - facultades: FacultadLite[] - onSaved?: () => void -}) { - const [saving, setSaving] = useState(false) - const [nombre, setNombre] = useState(carrera?.nombre ?? "") - const [semestres, setSemestres] = useState(carrera?.semestres ?? 9) - const [activo, setActivo] = useState(carrera?.activo ?? true) - const [facultadId, setFacultadId] = useState(carrera?.facultad_id ?? "none") +// ...existing code... - useEffect(() => { - if (mode === "edit" && carrera) { - setNombre(carrera.nombre) - setSemestres(carrera.semestres) - setActivo(carrera.activo) - setFacultadId(carrera.facultad_id ?? "none") - } else if (mode === "create") { - setNombre("") - setSemestres(9) - setActivo(true) - setFacultadId("none") - } - }, [mode, carrera, open]) - - async function save() { - if (!nombre.trim()) { - alert("Escribe un nombre") - return - } - setSaving(true) - const payload = { - nombre: nombre.trim(), - semestres: Number(semestres) || 9, - activo, - facultad_id: facultadId === "none" ? null : facultadId, - } - - const action = - mode === "create" - ? supabase.from("carreras").insert([payload]).select("id").single() - : supabase.from("carreras").update(payload).eq("id", carrera!.id).select("id").single() - - const { error } = await action - setSaving(false) - if (error) { - alert(error.message) - return - } - onOpenChange(false) - onSaved?.() - } - - return ( - - - - {mode === "create" ? "Nueva carrera" : "Editar carrera"} - - {mode === "create" ? "Crea una nueva carrera en la base de datos." : "Actualiza los datos de la carrera."} - - - -
-
- - setNombre(e.target.value)} placeholder="Ing. en Software" /> -
- -
-
- - setSemestres(parseInt(e.target.value || "9", 10))} /> -
-
- -
- - {activo ? "Activa" : "Inactiva"} -
-
-
- -
- - -
-
- - - - - -
-
- ) -} - -/* -------------------- Detalle (criterios) -------------------- */ -function CarreraDetailDialog({ - carrera, - onOpenChange, - onChanged, -}: { - carrera: CarreraRow | null - onOpenChange: (c: CarreraRow | null) => void - onChanged?: () => void -}) { - const carreraId = carrera?.id ?? "" - const { data: criterios = [], isFetching } = useQuery({ - ...criteriosOptions(carreraId || "noop"), - enabled: !!carreraId, - }) - const [q, setQ] = useState("") - const [newCritOpen, setNewCritOpen] = useState(false) - - const filtered = useMemo(() => { - const t = q.trim().toLowerCase() - if (!t) return criterios - return criterios.filter((c) => - [c.nombre, c.descripcion, c.tipo, c.referencia_documento].filter(Boolean).some((v) => String(v).toLowerCase().includes(t)) - ) - }, [q, criterios]) - - return ( - !o && onOpenChange(null)}> - - - {carrera?.nombre} - - {carrera?.facultades?.nombre ?? "—"} · {carrera?.semestres} semestres {typeof carrera?.activo === "boolean" && ( - - {carrera?.activo ? "Activa" : "Inactiva"} - - )} - - - -
-
-
- - setQ(e.target.value)} placeholder="Buscar criterio por nombre, tipo o referencia…" className="pl-8" /> -
- -
- - {isFetching ? ( -
Cargando criterios…
- ) : ( - <> -
{filtered.length} criterio(s){q ? " (filtrado)" : ""}
- - {filtered.length === 0 ? ( -
No hay criterios
- ) : ( - - {filtered.map((c) => ( - - -
- {c.nombre} -
- {c.tipo && {c.tipo}} - {c.obligatorio ? "Obligatorio" : "Opcional"} -
-
-
- - {c.descripcion &&

{c.descripcion}

} -
- {c.referencia_documento && ( - - - - Referencia - - - )} - {c.fecha_creacion && ( - - - {new Date(c.fecha_creacion).toLocaleString()} - - )} -
-
-
- ))} -
- )} - - )} -
- - - - - - {/* Crear criterio */} - -
-
- ) -} - -/* -------------------- Form crear criterio -------------------- */ -function CriterioFormDialog({ - open, - onOpenChange, - carreraId, - onSaved, -}: { - open: boolean - onOpenChange: (o: boolean) => void - carreraId: string - onSaved?: () => void -}) { - const qc = useQueryClient() - const [saving, setSaving] = useState(false) - const [nombre, setNombre] = useState("") - const [tipo, setTipo] = useState("") - const [descripcion, setDescripcion] = useState("") - const [obligatorio, setObligatorio] = useState(true) - const [referencia, setReferencia] = useState("") - - useEffect(() => { - if (!open) { - setNombre("") - setTipo("") - setDescripcion("") - setObligatorio(true) - setReferencia("") - } - }, [open]) - - async function save() { - if (!carreraId) return - if (!nombre.trim()) { - alert("Escribe un nombre") - return - } - setSaving(true) - const { error } = await supabase.from("criterios_carrera").insert([ - { - nombre: nombre.trim(), - tipo: tipo || null, - descripcion: descripcion || null, - obligatorio, - referencia_documento: referencia || null, - carrera_id: carreraId, - }, - ]) - setSaving(false) - if (error) { - alert(error.message) - return - } - onOpenChange(false) - await qc.invalidateQueries({ queryKey: criteriosKeys.byCarrera(carreraId) }) - onSaved?.() - } - - return ( - - - - Nuevo criterio - Agrega un criterio para esta carrera. - - -
-
- - setNombre(e.target.value)} placeholder="Infraestructura de laboratorios" /> -
-
- - setTipo(e.target.value)} placeholder="Académico / Operativo / Otro" /> -
-
- - setDescripcion(e.target.value)} placeholder="Detalle o alcance del criterio" /> -
-
-
- -
- - {obligatorio ? "Sí" : "No"} -
-
-
- - setReferencia(e.target.value)} placeholder="https://…" /> -
-
-
- - - - - -
-
- ) -} +// ...existing code... diff --git a/src/routes/_authenticated/dashboard.tsx b/src/routes/_authenticated/dashboard.tsx index 38c1efc..8a19e2b 100644 --- a/src/routes/_authenticated/dashboard.tsx +++ b/src/routes/_authenticated/dashboard.tsx @@ -279,7 +279,7 @@ function RouteComponent() {
- + Calidad de planes @@ -293,7 +293,7 @@ function RouteComponent() { - + Salud de asignaturas @@ -308,7 +308,7 @@ function RouteComponent() { {/* Actividad reciente */} - + Actividad reciente diff --git a/src/routes/_authenticated/plan/$planId.tsx b/src/routes/_authenticated/plan/$planId.tsx index 9d9e4be..9db7b38 100644 --- a/src/routes/_authenticated/plan/$planId.tsx +++ b/src/routes/_authenticated/plan/$planId.tsx @@ -79,7 +79,7 @@ function RouteComponent() {
- {plan.nombre} + {plan.nombre}
{showCarrera && plan.carreras?.nombre ? `Carrera: ${plan.carreras.nombre}` : null} {showFacultad && fac?.nombre ? `${showCarrera ? ' · ' : ''}Facultad: ${fac.nombre}` : null} @@ -113,7 +113,7 @@ function RouteComponent() {
- Asignaturas ({asignaturasCount}) + Asignaturas ({asignaturasCount})
{ qc.invalidateQueries({ queryKey: asignaturaKeys.count(plan.id) }) diff --git a/src/routes/_authenticated/planes.tsx b/src/routes/_authenticated/planes.tsx index adcb6fb..cc8e4db 100644 --- a/src/routes/_authenticated/planes.tsx +++ b/src/routes/_authenticated/planes.tsx @@ -78,7 +78,7 @@ function RouteComponent() {
- Planes de estudio + Planes de estudio
- Usuarios + Usuarios
setQ(e.target.value)} className="w-full" />