import { createFileRoute, Link } from "@tanstack/react-router" import { useEffect, useMemo, useRef, useState } from "react" import * as Icons from "lucide-react" import { useMutation, useQueryClient, useSuspenseQuery } from "@tanstack/react-query" import { supabase, useSupabaseAuth } from "@/auth/supabase" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { Badge } from "@/components/ui/badge" import { AcademicSections, planKeys } from "@/components/planes/academic-sections" import { GradientMesh } from "../../../components/planes/GradientMesh" import { asignaturaExtraOptions, asignaturaKeys, asignaturasCountOptions, asignaturasPreviewOptions, planByIdOptions, type AsignaturaLite, type PlanFull } from "@/components/planes/planQueries" import { softAccentStyle } from "@/components/planes/planHelpers" import { Dialog, DialogContent, DialogDescription, DialogTitle } from "@/components/ui/dialog" import { DialogFooter, DialogHeader } from "@/components/ui/dialog" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import confetti from "canvas-confetti" import { Textarea } from "@/components/ui/textarea" import { AuroraButton } from "@/components/effect/aurora-button" import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" import { DeletePlanButton } from "@/components/planes/DeletePlan" type LoaderData = { planId: string } export const Route = createFileRoute("/_authenticated/plan/$planId")({ component: RouteComponent, pendingComponent: PageSkeleton, loader: async ({ params, context: { queryClient } }): Promise => { const { planId } = params await Promise.all([ queryClient.ensureQueryData(planByIdOptions(planId)), queryClient.ensureQueryData(asignaturasCountOptions(planId)), queryClient.ensureQueryData(asignaturasPreviewOptions(planId)), ]) return { planId } }, }) // ...existing code... function RouteComponent() { const qc = useQueryClient() const { planId } = Route.useLoaderData() as LoaderData const auth = useSupabaseAuth() const { data: plan } = useSuspenseQuery(planByIdOptions(planId)) const { data: asignaturasCount } = useSuspenseQuery(asignaturasCountOptions(planId)) const { data: asignaturasPreview } = useSuspenseQuery(asignaturasPreviewOptions(planId)) const showFacultad = auth.claims?.role === 'lci' || auth.claims?.role === 'vicerrectoria' const showCarrera = auth.claims?.role === 'secretario_academico' const fac = plan.carreras?.facultades const accent = useMemo(() => softAccentStyle(fac?.color), [fac?.color]) const IconComp = (fac?.icon && (Icons as any)[fac.icon]) || Icons.Building2 const facColor = plan.carreras?.facultades?.color ?? null // Animaciones y refs pueden modularizarse si se desea const headerRef = useRef(null) const statsRef = useRef(null) useEffect(() => { // ...animaciones header y stats... }, []) return (
{plan.nombre}
{showCarrera && plan.carreras?.nombre ? `Carrera: ${plan.carreras.nombre}` : null} {showFacultad && fac?.nombre ? `${showCarrera ? ' · ' : ''}Facultad: ${fac.nombre}` : null}
{plan.estado && ( {plan.estado} )}
Asignaturas ({asignaturasCount})
{ qc.invalidateQueries({ queryKey: asignaturaKeys.count(plan.id) }) qc.invalidateQueries({ queryKey: asignaturaKeys.preview(plan.id) }) }} /> Ver en página de Asignaturas
{asignaturasPreview.length === 0 ? (
Sin asignaturas
) : (
{asignaturasPreview.map((a) => ( ))}
)}
) } function hexToRgbA(hex?: string | null, a = .25) { 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 fmt = (n?: number | null) => (n !== null && n !== undefined) ? Intl.NumberFormat().format(n) : "—" /* ===== UI bits ===== */ function StatCard({ label, value = "—", Icon = Icons.Info, accent, className = "", title }: { label: string value?: React.ReactNode Icon?: React.ComponentType> accent?: string | null className?: string title?: string }) { const border = hexToRgbA(accent, .28) const chipBg = hexToRgbA(accent, .08) const glow = hexToRgbA(accent, .14) return (
{label}
{value}
) } /* ===== Editar ===== */ function EditPlanButton({ plan }: { plan: PlanFull }) { const [open, setOpen] = useState(false) const [form, setForm] = useState>({}) const [saving, setSaving] = useState(false) const qc = useQueryClient() const mutation = useMutation({ mutationFn: async (payload: Partial) => { const { error } = await supabase.from('plan_estudios').update({ nombre: payload.nombre ?? plan.nombre, nivel: payload.nivel ?? plan.nivel, duracion: payload.duracion ?? plan.duracion, total_creditos: payload.total_creditos ?? plan.total_creditos, }).eq('id', plan.id) if (error) throw error }, onMutate: async (payload) => { await qc.cancelQueries({ queryKey: planKeys.byId(plan.id) }) const prev = qc.getQueryData(planKeys.byId(plan.id)) qc.setQueryData(planKeys.byId(plan.id), (old) => old ? { ...old, ...payload } as PlanFull : old as any) return { prev } }, onError: (_e, _vars, ctx) => { if (ctx?.prev) qc.setQueryData(planKeys.byId(plan.id), ctx.prev) }, onSettled: async () => { await qc.invalidateQueries({ queryKey: planKeys.byId(plan.id) }) } }) async function save() { setSaving(true) try { await mutation.mutateAsync(form) setOpen(false) } finally { setSaving(false) } } return ( <> Editar plan Actualiza datos básicos.
setForm({ ...form, nombre: e.target.value })} /> setForm({ ...form, nivel: e.target.value })} /> setForm({ ...form, duracion: e.target.value })} /> setForm({ ...form, total_creditos: Number(e.target.value) || null })} />
) } function Field({ label, children }: { label: string; children: React.ReactNode }) { return (
{children}
) } /* ===== Ajustar IA ===== */ function AdjustAIButton({ plan }: { plan: PlanFull }) { const [open, setOpen] = useState(false) const [prompt, setPrompt] = useState('') const [loading, setLoading] = useState(false) async function apply() { setLoading(true) await fetch(`${import.meta.env.VITE_BACK_ORIGIN}/api/mejorar/plan`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ prompt, plan_id: plan.id }), }).catch(() => { }) setLoading(false) confetti({ particleCount: 120, spread: 80, origin: { y: 0.6 } }) setOpen(false) } return ( <> Ajustar con IA Describe cómo quieres modificar el plan actual.