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

@@ -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.map((r) => (
<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">
{r.tipo === 'plan' ? <Icons.ScrollText className="w-4 h-4" /> : <Icons.BookOpen className="w-4 h-4" />}
{r.nombre ?? '—'}

View File

@@ -1,5 +1,5 @@
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 { Input } from "@/components/ui/input"
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 * as Icons from "lucide-react"
import { Plus, RefreshCcw, Building2, ScrollText, BookOpen } from "lucide-react"
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"
export type PlanDeEstudios = {
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)
return (data ?? []) as PlanRow[]
},
})
/* ---------- helpers de estilo suave ---------- */
@@ -62,10 +62,9 @@ function RouteComponent() {
const [q, setQ] = useState("")
const data = Route.useLoaderData() as PlanRow[]
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 showCarrera = auth.claims?.role === "secretario_academico"
const showCarrera = showFacultad || auth.claims?.role === "secretario_academico"
const filtered = useMemo(() => {
const term = q.trim().toLowerCase()
@@ -106,11 +105,7 @@ function RouteComponent() {
return (
<Link
key={p.id}
// Runtime navega con ?planId=... (abrimos el modal),
// pero la URL se enmascara SIN el search param:
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"
params={{ planId: p.id }}
style={styles}
@@ -131,12 +126,12 @@ function RouteComponent() {
<div className="flex items-center gap-2 text-xs">
{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}
</Badge>
)}
{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}
</Badge>
)}
@@ -157,95 +152,6 @@ function RouteComponent() {
)}
</CardContent>
</Card>
{/* MODAL: se muestra si existe ?planId=... */}
<PlanPreviewModal planId={search?.planId} onClose={() =>
router.navigate({ to: "/planes", replace: true })
} />
</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)
.single()
if (error) throw error
return data as PlanDetail
return data
},
})