feat: implement asignaturas management with dynamic routing and UI updates
This commit is contained in:
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user