feat: implement asignaturas management with dynamic routing and UI updates

This commit is contained in:
2025-08-22 08:10:49 -06:00
parent 8f46acd4b3
commit 9727f4c691
8 changed files with 249 additions and 212 deletions

View File

@@ -1,4 +1,4 @@
import { createFileRoute, Link, useParams } from '@tanstack/react-router'
import { createFileRoute, Link } from '@tanstack/react-router'
import { supabase, useSupabaseAuth } from '@/auth/supabase'
import * as Icons from 'lucide-react'
import { useEffect, useMemo, useRef, useState } from 'react'
@@ -25,7 +25,8 @@ type PlanFull = {
estado: string | null; fecha_creacion: string | null;
carreras: { id: string; nombre: string; facultades?: { id: string; nombre: string; color?: string | null; icon?: string | null } | null } | null
}
type LoaderData = { plan: PlanFull; asignaturasCount: number }
type AsignaturaLite = { id: string; nombre: string; semestre: number | null; creditos: number | null }
type LoaderData = { plan: PlanFull; asignaturasCount: number; asignaturasPreview: AsignaturaLite[] }
/* ============== ROUTE ============== */
export const Route = createFileRoute('/_authenticated/plan/$planId')({
@@ -49,7 +50,19 @@ export const Route = createFileRoute('/_authenticated/plan/$planId')({
.select('*', { count: 'exact', head: true })
.eq('plan_id', params.planId)
return { plan: plan as unknown as PlanFull, asignaturasCount: count ?? 0 }
const { data: asignaturasPreview } = await supabase
.from('asignaturas')
.select('id, nombre, semestre, creditos')
.eq('plan_id', params.planId)
.order('semestre', { ascending: true })
.order('nombre', { ascending: true })
.limit(8)
return {
plan: plan as unknown as PlanFull,
asignaturasCount: count ?? 0,
asignaturasPreview: (asignaturasPreview ?? []) as AsignaturaLite[],
}
},
})
@@ -112,7 +125,7 @@ function GradientMesh({ color }: { color?: string | null }) {
/* ============== PAGE ============== */
function RouteComponent() {
const { plan, asignaturasCount } = Route.useLoaderData() as LoaderData
const { plan, asignaturasCount, asignaturasPreview } = Route.useLoaderData() as LoaderData
const auth = useSupabaseAuth()
const showFacultad = auth.claims?.role === 'lci' || auth.claims?.role === 'vicerrectoria'
const showCarrera = auth.claims?.role === 'secretario_academico'
@@ -144,7 +157,7 @@ function RouteComponent() {
// Stats y campos con ScrollTrigger
if (statsRef.current) {
const ctx = gsap.context(() => {
gsap.from('.kv', {
gsap.from('.academics', {
y: 14, opacity: 0, stagger: .08, duration: .4,
scrollTrigger: { trigger: statsRef.current, start: 'top 85%' }
})
@@ -204,8 +217,8 @@ function RouteComponent() {
</Badge>
)}
<Link
to="/asignaturas"
search={{ planId: plan.id }}
to="/asignaturas/$planId"
params={{ planId: plan.id }}
className="inline-flex items-center gap-2 rounded-xl border px-3 py-2 text-sm hover:bg-neutral-50"
>
<Icons.BookOpen className="w-4 h-4" /> Ver asignaturas
@@ -220,23 +233,58 @@ function RouteComponent() {
{/* stats */}
<CardContent
ref={statsRef}
className="relative z-10 grid gap-3 [grid-template-columns:repeat(auto-fit,minmax(180px,1fr))]"
>
<StatCard label="Nivel" value={plan.nivel ?? "—"} Icon={Icons.GraduationCap} accent={facColor} />
<StatCard label="Duración" value={plan.duracion ?? "—"} Icon={Icons.Clock} accent={facColor} />
<StatCard label="Créditos" value={fmt(plan.total_creditos)} Icon={Icons.Coins} accent={facColor} />
<StatCard label="Asignaturas" value={fmt(asignaturasCount)} Icon={Icons.BookOpen} accent={facColor} />
<StatCard
label="Creado"
value={plan.fecha_creacion ? new Date(plan.fecha_creacion).toLocaleDateString() : "—"}
Icon={Icons.CalendarDays}
accent={facColor}
/>
<div className="academics relative z-10 grid gap-3 [grid-template-columns:repeat(auto-fit,minmax(180px,1fr))]">
<StatCard label="Nivel" value={plan.nivel ?? "—"} Icon={Icons.GraduationCap} accent={facColor} />
<StatCard label="Duración" value={plan.duracion ?? "—"} Icon={Icons.Clock} accent={facColor} />
<StatCard label="Créditos" value={fmt(plan.total_creditos)} Icon={Icons.Coins} accent={facColor} />
<StatCard
label="Creado"
value={plan.fecha_creacion ? new Date(plan.fecha_creacion).toLocaleDateString() : "—"}
Icon={Icons.CalendarDays}
accent={facColor}
/>
</div>
</CardContent>
</Card>
<AcademicSections planId={plan.id} plan={plan} color={fac?.color} />
<div className="academics">
<AcademicSections planId={plan.id} plan={plan} color={fac?.color} />
</div>
<Card className="border shadow-sm">
<CardHeader className="flex items-center justify-between gap-2">
<CardTitle className="text-base">Asignaturas ({asignaturasCount})</CardTitle>
{/* Abre el modal enmascarado */}
<Link
to="/asignaturas/$planId"
params={{ planId: plan.id }}
className="inline-flex items-center gap-2 rounded-xl border px-3 py-2 text-sm hover:bg-neutral-50"
>
<Icons.BookOpen className="w-4 h-4" /> Ver todas
</Link>
</CardHeader>
<CardContent className="flex flex-wrap gap-2">
{asignaturasPreview.length === 0 && (
<div className="text-sm text-neutral-500">Sin asignaturas</div>
)}
{asignaturasPreview.map(a => (
<Link
key={a.id}
to="/asignaturas/$planId"
params={{ planId: plan.id }}
className="rounded-full border px-3 py-1 text-xs bg-white/70 hover:bg-white transition"
title={a.nombre}
>
{a.semestre ? `S${a.semestre} · ` : ''}{a.nombre}
</Link>
))}
</CardContent>
</Card>
</div>
)
}