import { createFileRoute, Link, useNavigate, useRouter } from '@tanstack/react-router' import { useSuspenseQuery, queryOptions, useQueryClient } from '@tanstack/react-query' import { supabase, useSupabaseAuth } from '@/auth/supabase' import * as Icons from 'lucide-react' import { useMemo } from 'react' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Badge } from '@/components/ui/badge' /* ========= Tipos ========= */ type Plan = { id: string nombre: string fecha_creacion: string | null objetivo_general: string | null perfil_ingreso: string | null perfil_egreso: string | null sistema_evaluacion: string | null total_creditos: number | null } type Asignatura = { id: string nombre: string fecha_creacion: string | null contenidos: any | null criterios_evaluacion: string | null bibliografia: any | null } type LoaderData = { kpis: { facultades: number; carreras: number; planes: number; asignaturas: number } calidadPlanesPct: number saludAsignaturas: { sinBibliografia: number; sinCriterios: number; sinContenidos: number } recientes: Array<{ tipo: 'plan' | 'asignatura'; id: string; nombre: string; fecha: string | null }> } /* ========= Query Key & Fetcher ========= */ const dashboardKeys = { root: ['dashboard'] as const, summary: () => [...dashboardKeys.root, 'summary'] as const, } async function fetchDashboard(): Promise { const [facRes, carRes, planesRes, asigRes] = await Promise.all([ supabase.from('facultades').select('*', { count: 'exact', head: true }), supabase.from('carreras').select('*', { count: 'exact', head: true }), supabase .from('plan_estudios') .select( 'id, nombre, fecha_creacion, objetivo_general, perfil_ingreso, perfil_egreso, sistema_evaluacion, total_creditos' ), supabase .from('asignaturas') .select('id, nombre, fecha_creacion, contenidos, criterios_evaluacion, bibliografia'), ]) const planes = (planesRes.data ?? []) as Plan[] const asignaturas = (asigRes.data ?? []) as Asignatura[] // Calidad de planes const needed: (keyof Plan)[] = [ 'objetivo_general', 'perfil_ingreso', 'perfil_egreso', 'sistema_evaluacion', 'total_creditos', ] const completos = planes.filter((p) => needed.every((k) => p[k] !== null && String(p[k] ?? '').trim() !== '')).length const calidadPlanesPct = planes.length ? Math.round((completos / planes.length) * 100) : 0 // Salud de asignaturas const sinBibliografia = asignaturas.filter( (a) => !a.bibliografia || (Array.isArray(a.bibliografia) && a.bibliografia.length === 0) ).length const sinCriterios = asignaturas.filter((a) => !a.criterios_evaluacion?.trim()).length const sinContenidos = asignaturas.filter( (a) => !a.contenidos || (Array.isArray(a.contenidos) && a.contenidos.length === 0) ).length // Actividad reciente (últimos 8 ítems) const recientes = [ ...planes.map((p) => ({ tipo: 'plan' as const, id: p.id, nombre: p.nombre, fecha: p.fecha_creacion })), ...asignaturas.map((a) => ({ tipo: 'asignatura' as const, id: a.id, nombre: a.nombre, fecha: a.fecha_creacion })), ] .sort((a, b) => new Date(b.fecha ?? 0).getTime() - new Date(a.fecha ?? 0).getTime()) .slice(0, 8) return { kpis: { facultades: facRes.count ?? 0, carreras: carRes.count ?? 0, planes: planes.length, asignaturas: asignaturas.length, }, calidadPlanesPct, saludAsignaturas: { sinBibliografia, sinCriterios, sinContenidos }, recientes, } } const dashboardOptions = () => queryOptions({ queryKey: dashboardKeys.summary(), queryFn: fetchDashboard, staleTime: 30_000 }) /* ========= Ruta ========= */ export const Route = createFileRoute('/_authenticated/dashboard')({ component: RouteComponent, pendingComponent: DashboardSkeleton, loader: async ({ context: { queryClient } }) => { await queryClient.ensureQueryData(dashboardOptions()) return null }, }) /* ========= Helpers visuales ========= */ function gradient(bg = '#2563eb') { return { background: `linear-gradient(135deg, ${bg} 0%, ${bg}cc 45%, ${bg}a6 75%, ${bg}66 100%)` } as React.CSSProperties } function hex(color?: string | null, fallback = '#2563eb') { return color && /^#([0-9a-f]{6}|[0-9a-f]{3})$/i.test(color) ? color : fallback } function Ring({ pct, color }: { pct: number; color: string }) { const R = 42 const C = 2 * Math.PI * R const off = C * (1 - Math.min(Math.max(pct, 0), 100) / 100) return (
{pct}%
Planes con información clave completa
) } function Tile({ to, label, value, Icon }: { to: string; label: string; value: number | string; Icon: React.ComponentType> }) { return (
{label}
{value}
) } /* ========= Página ========= */ function RouteComponent() { const { data } = useSuspenseQuery(dashboardOptions()) const { kpis, calidadPlanesPct, saludAsignaturas, recientes } = data const auth = useSupabaseAuth() const router = useRouter() const qc = useQueryClient() const primary = hex(auth.claims?.facultad_color, '#1d4ed8') const name = auth.user?.user_metadata?.nombre || auth.user?.email?.split('@')[0] || '¡Hola!' const isAdmin = auth.claims?.role === 'lci' || auth.claims?.role === 'vicerrectoria' const role = auth.claims?.role as 'lci' | 'vicerrectoria' | 'secretario_academico' | 'jefe_carrera' | 'planeacion' | undefined const navigate = useNavigate({ from: Route.fullPath }) // Mensaje contextual const roleHint = useMemo(() => { switch (role) { case 'vicerrectoria': return 'Panorama académico, calidad y actividad reciente.' case 'secretario_academico': return 'Enfócate en tu facultad: salud de asignaturas y avance de planes.' case 'jefe_carrera': return 'Accede rápido a planes y asignaturas de tu carrera.' case 'planeacion': return 'Monitorea consistencia de planes y evidencias de evaluación.' default: return 'Atajos para crear, revisar y mejorar contenido.' } }, [role]) return (
{/* Header con saludo y búsqueda global */}

Hola, {name}

{roleHint}

{role && ( {role} )} {isAdmin && ( admin )}
{ if (e.key === 'Enter') { const q = (e.target as HTMLInputElement).value.trim() if (!q) return navigate({ to: '/planes', search: { plan: q } }) } }} />
{/* Atajos rápidos (según rol) */}
Nuevo plan Nueva asignatura {isAdmin && ( Invitar usuario )}
{/* KPIs principales */}
{/* Calidad + Salud */}
Calidad de planes

Considera objetivo general, perfiles, sistema de evaluación y créditos.

Salud de asignaturas
{/* Actividad reciente */} Actividad reciente {recientes.length === 0 &&
Sin actividad registrada.
}
    {recientes.map((r) => (
  • {r.tipo === 'plan' ? ( ) : ( )} {r.nombre} {r.fecha ? new Date(r.fecha).toLocaleDateString() : ''}
  • ))}
) } /* ========= Subcomponentes ========= */ function HealthRow({ label, value, to }: { label: string; value: number; to: string }) { const warn = value > 0 return ( {label} {value} ) } /* ========= Skeleton (cuando carga) ========= */ function Pulse({ className = '' }: { className?: string }) { return
} function DashboardSkeleton() { return (
{Array.from({ length: 4 }).map((_, i) => ( ))}
) }