2 Commits

Author SHA1 Message Date
Guillermo.Arrieta d3267e5890 wip 2026-04-13 11:04:28 -06:00
Guillermo.Arrieta b77906e2e9 wip 2026-03-27 19:32:37 -06:00
9 changed files with 300 additions and 282 deletions
+20 -3
View File
@@ -14,6 +14,7 @@ interface PlanEstudiosCardProps {
facultad: string
estado: string
claseColorEstado?: string
colorEstadoHex?: string
colorFacultad: string
onClick?: () => void
}
@@ -26,9 +27,17 @@ export default function PlanEstudiosCard({
facultad,
estado,
claseColorEstado = '',
colorEstadoHex,
colorFacultad,
onClick,
}: PlanEstudiosCardProps) {
const badgeStyle = colorEstadoHex
? ({
backgroundColor: colorEstadoHex,
borderColor: colorEstadoHex,
} as const)
: undefined
return (
<Card
onClick={onClick}
@@ -36,7 +45,7 @@ export default function PlanEstudiosCard({
'group relative flex h-full cursor-pointer flex-col justify-between overflow-hidden transition-all hover:shadow-lg',
)}
>
<div className="flex flex-grow flex-col">
<div className="flex grow flex-col">
<CardHeader className="pb-2">
{/* Círculo del ícono con el color de la facultad */}
<div
@@ -63,8 +72,16 @@ export default function PlanEstudiosCard({
</div>
<CardFooter className="flex items-center justify-between pt-0 pb-6">
<Badge className={cn('text-sm font-semibold', claseColorEstado)}>
{estado}
<Badge
style={badgeStyle}
className={cn(
'text-sm font-semibold',
!colorEstadoHex && claseColorEstado,
)}
>
<span className="text-white [text-shadow:1px_1px_0_#000,-1px_-1px_0_#000,1px_-1px_0_#000,-1px_1px_0_#000,0_1px_0_#000,0_-1px_0_#000,1px_0_0_#000,-1px_0_0_#000]">
{estado}
</span>
</Badge>
{/* Flecha animada */}
-26
View File
@@ -1,26 +0,0 @@
import { StatusBadge } from "./StatusBadge";
export function PlanCard({ plan }: any) {
return (
<div className="bg-[#eaf6fa] rounded-2xl p-6 border hover:shadow-md transition">
<div className="flex justify-between items-start mb-4">
<span className="text-sm text-gray-500"> Ingeniería</span>
<StatusBadge status={plan.status} />
</div>
<h3 className="text-lg font-semibold text-gray-900">
{plan.title}
</h3>
<p className="text-sm text-gray-600 mb-6">
{plan.subtitle}
</p>
<div className="flex justify-between text-sm text-gray-500">
<span>{plan.cycles} ciclos</span>
<span>{plan.credits} créditos</span>
<span></span>
</div>
</div>
)
}
-38
View File
@@ -1,38 +0,0 @@
import { PlanCard } from './PlanCard'
const mockPlans = [
{
id: 1,
name: 'Ingeniería en Sistemas',
level: 'Licenciatura',
status: 'Activo',
},
{
id: 2,
name: 'Arquitectura',
level: 'Licenciatura',
status: 'Activo',
},
{
id: 3,
name: 'Maestría en Educación',
level: 'Maestría',
status: 'Inactivo',
},
]
export function PlanGrid() {
return (
<div>
<h2 className="text-lg font-semibold mb-4">
Planes disponibles
</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{mockPlans.map(plan => (
<PlanCard key={plan.id} plan={plan} />
))}
</div>
</div>
)
}
-11
View File
@@ -1,11 +0,0 @@
export function StatCard({ icon, value, label }: any) {
return (
<div className="bg-white rounded-xl border p-6">
<div className="flex items-center gap-3 mb-2">
<span className="text-xl">{icon}</span>
<span className="text-2xl font-semibold">{value}</span>
</div>
<p className="text-sm text-gray-500">{label}</p>
</div>
)
}
-12
View File
@@ -1,12 +0,0 @@
import { StatCard } from "./StatCard";
export function StatsGrid() {
return (
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
<StatCard icon="📘" value="12" label="Planes Activos" />
<StatCard icon="🕒" value="4" label="En Revisión" />
<StatCard icon="✅" value="8" label="Aprobados" />
<StatCard icon="👥" value="6" label="Carreras" />
</div>
)
}
-19
View File
@@ -1,19 +0,0 @@
export function StatusBadge({ status }: { status: string }) {
const styles: any = {
revision: 'bg-blue-100 text-blue-700',
aprobado: 'bg-green-100 text-green-700',
borrador: 'bg-gray-200 text-gray-600',
}
return (
<span
className={`text-xs px-3 py-1 rounded-full font-medium ${styles[status]}`}
>
{status === 'revision'
? 'En Revisión'
: status === 'aprobado'
? 'Aprobado'
: 'Borrador'}
</span>
)
}
+80 -50
View File
@@ -8,18 +8,24 @@ import {
} from 'lucide-react'
import { useState, useEffect, forwardRef } from 'react'
import type { Database } from '@/types/supabase'
import { Badge } from '@/components/ui/badge'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { NotFoundPage } from '@/components/ui/NotFoundPage'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { Skeleton } from '@/components/ui/skeleton'
import { plans_get } from '@/data/api/plans.api'
import { usePlan, useUpdatePlanFields } from '@/data/hooks/usePlans'
import { qk } from '@/data/query/keys'
import { cn } from '@/lib/utils'
type NivelPlanEstudio = Database['public']['Enums']['nivel_plan_estudio']
export const Route = createFileRoute('/planes/$planId/_detalle')({
loader: async ({ context: { queryClient }, params: { planId } }) => {
@@ -54,28 +60,26 @@ function RouteComponent() {
// Estados locales para manejar la edición "en vivo" antes de persistir
const [nombrePlan, setNombrePlan] = useState('')
const [nivelPlan, setNivelPlan] = useState('')
const [isDirty, setIsDirty] = useState(false)
const [nivelPlan, setNivelPlan] = useState<NivelPlanEstudio | undefined>(
undefined,
)
useEffect(() => {
if (data) {
setNombrePlan(data.nombre || '')
setNivelPlan(data.nivel || '')
setNivelPlan(data.nivel)
}
}, [data])
const niveles = [
const niveles: Array<NivelPlanEstudio> = [
'Licenciatura',
'Maestría',
'Doctorado',
'Diplomado',
'Especialidad',
'Diplomado',
'Otro',
]
const persistChange = (patch: any) => {
mutate({ planId, patch })
}
const MAX_CHARACTERS = 200
const handleKeyDown = (e: React.KeyboardEvent<HTMLSpanElement>) => {
@@ -148,18 +152,18 @@ function RouteComponent() {
contentEditable
suppressContentEditableWarning
spellCheck={false}
aria-label="Nombre del plan"
title="Nombre del plan"
onKeyDown={handleKeyDown}
onPaste={handlePaste}
onBlur={(e) => {
const nuevoNombre =
e.currentTarget.textContent?.trim() || ''
const nuevoNombre = e.currentTarget.textContent.trim()
setNombrePlan(nuevoNombre)
if (nuevoNombre !== data?.nombre) {
mutate({ planId, patch: { nombre: nuevoNombre } })
}
}}
className="hover:border-input focus:border-primary block w-full cursor-text border-b border-transparent break-words whitespace-pre-wrap transition-colors outline-none select-text sm:inline-block sm:w-auto"
style={{ textDecoration: 'none' }}
className="hover:border-input focus:border-primary block w-full cursor-text border-b border-transparent wrap-break-word whitespace-pre-wrap no-underline transition-colors outline-none select-text sm:inline-block sm:w-auto"
>
{nombrePlan}
</span>
@@ -170,42 +174,68 @@ function RouteComponent() {
</p>
</div>
<div className="flex gap-2">
<Badge className="border-primary/20 bg-primary/10 text-primary hover:bg-primary/20 gap-1 px-3">
{data?.estados_plan?.etiqueta}
</Badge>
</div>
{(() => {
const estadoColorHex = (data?.estados_plan as any)?.color as
| string
| undefined
const badgeStyle = estadoColorHex
? ({
backgroundColor: estadoColorHex,
borderColor: estadoColorHex,
} as const)
: undefined
return (
<Badge
style={badgeStyle}
className={cn(
'text-sm font-semibold',
!estadoColorHex &&
'border-primary/20 bg-primary/10 text-primary hover:bg-primary/20',
)}
>
<span className="text-white [text-shadow:1px_1px_0_#000,-1px_-1px_0_#000,1px_-1px_0_#000,-1px_1px_0_#000,0_1px_0_#000,0_-1px_0_#000,1px_0_0_#000,-1px_0_0_#000]">
{data?.estados_plan?.etiqueta}
</span>
</Badge>
)
})()}
</div>
)}
{/* 3. Cards de Información */}
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-4">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<InfoCard
icon={<GraduationCap className="text-muted-foreground" />}
label="Nivel"
<div className="border-border/60 bg-muted/30 flex h-18 w-full items-center gap-4 rounded-xl border p-4 shadow-sm transition-all">
<div className="bg-background flex h-10 w-10 shrink-0 items-center justify-center rounded-lg border shadow-sm">
<GraduationCap className="text-muted-foreground" />
</div>
<div className="min-w-0 flex-1">
<p className="text-muted-foreground mb-0.5 truncate text-[10px] font-bold tracking-wider uppercase">
Nivel
</p>
<Select
value={nivelPlan}
isEditable
/>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-48">
{niveles.map((n) => (
<DropdownMenuItem
key={n}
onClick={() => {
setNivelPlan(n)
if (n !== data?.nivel) {
mutate({ planId, patch: { nivel: n } })
}
}}
>
{n}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
onValueChange={(value) => {
const nuevoNivel = value as NivelPlanEstudio
setNivelPlan(nuevoNivel)
if (nuevoNivel !== data?.nivel) {
mutate({ planId, patch: { nivel: nuevoNivel } })
}
}}
>
<SelectTrigger className="w-full" size="sm">
<SelectValue placeholder="---" />
</SelectTrigger>
<SelectContent>
{niveles.map((n) => (
<SelectItem key={n} value={n}>
{n}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<InfoCard
icon={<Clock className="text-muted-foreground" />}
@@ -220,7 +250,7 @@ function RouteComponent() {
<InfoCard
icon={<CalendarDays className="text-muted-foreground" />}
label="Creación"
value={data?.creado_en?.split('T')[0]}
value={data?.creado_en.split('T')[0]}
/>
</div>
@@ -6,12 +6,21 @@ import {
useParams,
useRouterState,
} from '@tanstack/react-router'
import { ArrowLeft, GraduationCap } from 'lucide-react'
import {
ArrowLeft,
GraduationCap,
Pencil,
Hash,
BookOpen,
CalendarDays,
Tag,
} from 'lucide-react'
import { useEffect, useState } from 'react'
import { Badge } from '@/components/ui/badge'
import { lateralConfetti } from '@/components/ui/lateral-confetti'
import { useSubject, useUpdateAsignatura } from '@/data'
import { cn } from '@/lib/utils'
export const Route = createFileRoute(
'/planes/$planId/asignaturas/$asignaturaId',
@@ -19,65 +28,147 @@ export const Route = createFileRoute(
component: AsignaturaLayout,
})
function EditableHeaderField({
// --- 1. COMPONENTE PARA EDITAR EL TÍTULO SOBRE FONDO AZUL ---
function InlineEditTitle({
value,
onSave,
className,
}: {
value: string | number
value: string
onSave: (val: string) => void
className?: string
}) {
const textValue = String(value)
const [isEditing, setIsEditing] = useState(false)
const [tempVal, setTempVal] = useState(value)
// Manejador para cuando el usuario termina de editar (pierde el foco)
const handleBlur = (e: React.FocusEvent<HTMLSpanElement>) => {
const newValue = e.currentTarget.innerText
if (newValue !== textValue) {
onSave(newValue)
}
useEffect(() => setTempVal(value), [value])
const handleSave = () => {
setIsEditing(false)
if (tempVal.trim() && tempVal !== value) onSave(tempVal.trim())
else setTempVal(value) // Revertir si está vacío
}
const handleKeyDown = (e: React.KeyboardEvent<HTMLSpanElement>) => {
if (e.key === 'Enter') {
e.preventDefault()
e.currentTarget.blur() // Forzamos el guardado al presionar Enter
}
if (isEditing) {
return (
<input
autoFocus
value={tempVal}
onChange={(e) => setTempVal(e.target.value)}
onBlur={handleSave}
onKeyDown={(e) => {
if (e.key === 'Enter') handleSave()
if (e.key === 'Escape') {
setTempVal(value)
setIsEditing(false)
}
}}
// Input estilizado para fondo oscuro: borde blanco sutil, texto blanco
className="focus:ring-primary/40 w-full rounded-md border-2 border-white/20 bg-transparent px-2 py-1 text-3xl font-bold text-white shadow-sm outline-none focus:ring-4"
/>
)
}
return (
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
<span
contentEditable
suppressContentEditableWarning={true} // Evita el warning de React por tener hijos y contentEditable
spellCheck={false}
onBlur={handleBlur}
onKeyDown={handleKeyDown}
className={`inline-block cursor-text rounded-sm px-1 transition-all hover:bg-white/10 focus:bg-white/20 focus:ring-2 focus:ring-blue-400/50 focus:outline-none ${className ?? ''} `}
<h1
onClick={() => setIsEditing(true)}
// Texto blanco por defecto, fondo blanco sutil al hover
className="group flex cursor-pointer items-center gap-3 rounded-md px-2 py-1 text-3xl font-bold text-white transition-colors hover:bg-white/5"
>
{textValue}
</span>
{value}
{/* Lápiz blanco sutil */}
<Pencil className="h-5 w-5 text-white/50 opacity-0 transition-all group-hover:opacity-100 hover:text-white" />
</h1>
)
}
// --- 2. COMPONENTE PARA EDITAR LOS BADGES SOBRE FONDO AZUL ---
function InlineEditBadge({
icon,
label,
value,
suffix = '',
type = 'text',
onSave,
}: {
icon: React.ReactNode
label: string
value: string | number
suffix?: string
type?: 'text' | 'number'
onSave: (val: string) => void
}) {
const [isEditing, setIsEditing] = useState(false)
const [tempVal, setTempVal] = useState(value)
useEffect(() => setTempVal(value), [value])
const handleSave = () => {
setIsEditing(false)
if (String(tempVal).trim() !== String(value)) {
onSave(String(tempVal))
}
}
if (isEditing) {
return (
// Contenedor del input con estética de badge oscuro
<div className="focus:ring-primary/40 flex h-8 items-center gap-2 rounded-md border border-white/20 bg-white/5 px-3 shadow-sm ring-1 focus-within:ring-2">
<span className="text-xs font-medium tracking-wider text-white/60 uppercase">
{label}:
</span>
<input
autoFocus
type={type}
value={tempVal}
onChange={(e) => setTempVal(e.target.value)}
onBlur={handleSave}
onKeyDown={(e) => {
if (e.key === 'Enter') handleSave()
if (e.key === 'Escape') {
setTempVal(value)
setIsEditing(false)
}
}}
// Texto blanco dentro del input
className="w-16 bg-transparent text-sm font-semibold text-white outline-none"
/>
</div>
)
}
return (
<button
onClick={() => setIsEditing(true)}
// Badge oscuro: borde blanco sutil, texto blanco, fondo más claro al hover
className="group flex h-8 items-center gap-2 rounded-md border border-white/10 bg-white/5 px-3 text-sm text-white transition-all hover:border-white/20 hover:bg-white/10"
>
{/* Ícono blanco sutil */}
<span className="text-white/70">{icon}</span>
<span className="text-xs font-medium tracking-wider text-white/60 uppercase">
{label}:
</span>
<span className="font-semibold text-white">
{value} {suffix}
</span>
{/* Lápiz blanco sutil */}
<Pencil className="h-3 w-3 text-white/50 opacity-0 transition-opacity group-hover:opacity-100" />
</button>
)
}
interface DatosPlan {
nombre?: string
}
function AsignaturaLayout() {
const location = useLocation()
const { asignaturaId } = useParams({
from: '/planes/$planId/asignaturas/$asignaturaId',
})
const { planId } = useParams({
const { asignaturaId, planId } = useParams({
from: '/planes/$planId/asignaturas/$asignaturaId',
})
const { data: asignaturaApi, isLoading: loadingAsig } =
useSubject(asignaturaId)
// 1. Asegúrate de tener estos estados en tu componente principal
const updateAsignatura = useUpdateAsignatura()
// Dentro de AsignaturaDetailPage
const [headerData, setHeaderData] = useState({
codigo: '',
nombre: '',
@@ -85,7 +176,6 @@ function AsignaturaLayout() {
ciclo: 0,
})
// Sincronizar cuando llegue la API
useEffect(() => {
if (asignaturaApi) {
setHeaderData({
@@ -102,23 +192,15 @@ function AsignaturaLayout() {
setHeaderData(newData)
const patch: Record<string, any> =
key === 'ciclo'
? { numero_ciclo: value }
: {
[key]: value,
}
key === 'ciclo' ? { numero_ciclo: value } : { [key]: value }
updateAsignatura.mutate({
asignaturaId,
patch,
})
updateAsignatura.mutate({ asignaturaId, patch })
}
const pathname = useRouterState({
select: (state) => state.location.pathname,
})
// Confetti al llegar desde creación IA
useEffect(() => {
if ((location.state as any)?.showConfetti) {
lateralConfetti()
@@ -128,101 +210,94 @@ function AsignaturaLayout() {
if (loadingAsig) {
return (
<div className="flex h-screen items-center justify-center bg-[#0b1d3a] text-white">
<div className="bg-background text-foreground flex h-screen items-center justify-center">
Cargando asignatura...
</div>
)
}
// Si no hay datos y no está cargando, algo falló
if (!asignaturaApi) return null
return (
<div>
<section className="bg-linear-to-b from-[#0b1d3a] to-[#0e2a5c] text-white">
<div className="mx-auto p-4 py-10 md:px-6 lg:px-8">
<div className="bg-background min-h-screen">
{/* HEADER DE LA ASIGNATURA CON TU FONDO AZUL HARDCODEADO */}
<section className="border-border border-b bg-[#0b1d3a] pt-6 pb-8">
<div className="mx-auto px-4 md:px-6 lg:px-8">
<Link
to="/planes/$planId/asignaturas"
params={{ planId }}
className="mb-4 flex items-center gap-2 text-sm text-blue-200 hover:text-white"
// Enlace blanco sutil
className="mb-4 flex w-fit items-center gap-2 text-sm text-white/70 transition-colors hover:text-white"
>
<ArrowLeft className="h-4 w-4" /> Volver al plan
</Link>
<div className="flex items-start justify-between gap-6">
<div className="space-y-3">
{/* CÓDIGO EDITABLE */}
<Badge className="border border-blue-700 bg-blue-900/50">
<EditableHeaderField
value={headerData.codigo}
onSave={(val) => handleUpdateHeader('codigo', val)}
/>
</Badge>
{/* NOMBRE EDITABLE */}
<h1 className="text-3xl font-bold">
<EditableHeaderField
value={headerData.nombre}
onSave={(val) => handleUpdateHeader('nombre', val)}
/>
</h1>
{
// console.log(headerData),
console.log(asignaturaApi.planes_estudio?.nombre)
}
<div className="flex flex-wrap gap-4 text-sm text-blue-200">
<span className="flex items-center gap-1">
<GraduationCap className="h-4 w-4 shrink-0" />
Pertenece al plan:{' '}
<span className="text-blue-100">
{(asignaturaApi.planes_estudio as DatosPlan).nombre || ''}
</span>
</span>
</div>
<div className="flex flex-col gap-4">
{/* Título Editable (Texto blanco controlado dentro del componente) */}
<div className="-ml-2">
<InlineEditTitle
value={headerData.nombre}
onSave={(val) => handleUpdateHeader('nombre', val)}
/>
</div>
<div className="flex flex-col items-end gap-2 text-right">
{/* CRÉDITOS EDITABLES */}
<Badge variant="secondary" className="gap-1">
<span className="inline-flex max-w-fit">
<EditableHeaderField
value={headerData.creditos}
onSave={(val) =>
handleUpdateHeader('creditos', parseInt(val) || 0)
}
/>
</span>
<span>créditos</span>
{/* Fila de Metadatos Alineados */}
<div className="flex flex-wrap items-center gap-3">
{/* Badge Estático del Tipo (Estilo oscuro sutil) */}
<Badge
variant="outline"
className="flex h-8 items-center gap-1.5 border-white/10 bg-white/5 px-3 text-white hover:border-white/20 hover:bg-white/10"
>
<Tag size={12} className="text-white/70" />
{asignaturaApi.tipo}
</Badge>
{/* SEMESTRE EDITABLE */}
<Badge variant="secondary" className="gap-1">
<EditableHeaderField
value={headerData.ciclo}
onSave={(val) =>
handleUpdateHeader('ciclo', parseInt(val) || 0)
}
/>
<span>° ciclo</span>
</Badge>
{/* Badges Editables (Texto blanco controlado dentro de los componentes) */}
<InlineEditBadge
icon={<Hash size={14} />}
label="Código"
value={headerData.codigo}
onSave={(val) => handleUpdateHeader('codigo', val)}
/>
<Badge variant="secondary">{asignaturaApi.tipo}</Badge>
<InlineEditBadge
icon={<BookOpen size={14} />}
label="Créditos"
type="number"
value={headerData.creditos}
onSave={(val) =>
handleUpdateHeader('creditos', parseInt(val) || 0)
}
/>
<InlineEditBadge
icon={<CalendarDays size={14} />}
label="Semestre"
type="number"
value={headerData.ciclo}
suffix="°"
onSave={(val) =>
handleUpdateHeader('ciclo', parseInt(val) || 0)
}
/>
</div>
{/* Subtítulo de contexto (Texto blanco sutil) */}
<div className="mt-2 flex items-center gap-2 text-sm text-white/70">
<GraduationCap className="h-4 w-4 shrink-0 text-white/60" />
<span>Pertenece al plan:</span>
<span className="font-medium text-white">
{(asignaturaApi.planes_estudio as DatosPlan).nombre || ''}
</span>
</div>
</div>
</div>
</section>
{/* TABS */}
<nav className="sticky top-0 z-20 border-b bg-white">
<div className="mx-auto p-4 py-2 md:px-6 lg:px-8">
{/* CAMBIOS CLAVE:
1. overflow-x-auto: Permite scroll horizontal.
2. scrollbar-hide: (Opcional) para que no se vea la barra fea.
3. justify-start md:justify-center: Alineado a la izquierda en móvil para que el scroll funcione, centrado en desktop.
*/}
<div className="no-scrollbar flex items-center justify-start gap-8 overflow-x-auto whitespace-nowrap md:justify-start">
{/* TABS NAVEGACIÓN (Se mantiene semántico para el cuerpo de la página) */}
<nav className="border-border bg-background/80 sticky top-0 z-20 border-b backdrop-blur-md">
<div className="mx-auto px-4 py-1 md:px-6 lg:px-8">
<div className="scrollbar-hide flex items-center justify-start gap-8 overflow-x-auto whitespace-nowrap md:justify-start">
{[
{ label: 'Datos', to: '' },
{ label: 'Contenido', to: 'contenido' },
@@ -246,11 +321,12 @@ function AsignaturaLayout() {
}
from="/planes/$planId/asignaturas/$asignaturaId"
params={{ planId, asignaturaId }}
className={`shrink-0 border-b-2 py-4 text-sm font-medium transition-colors ${
className={cn(
'shrink-0 border-b-2 py-3 text-sm font-medium transition-colors',
isActive
? 'border-blue-600 text-blue-600'
: 'border-transparent text-slate-500 hover:border-slate-300 hover:text-slate-700'
}`}
? 'border-primary text-primary font-bold'
: 'text-muted-foreground hover:border-border hover:text-foreground border-transparent',
)}
>
{tab.label}
</Link>
+5 -4
View File
@@ -224,9 +224,9 @@ function RouteComponent() {
// NOTA: El color del estado no viene en BD por defecto,
// puedes crear un mapa de colores o agregar columna 'color' a tabla 'estados_plan'
// Aquí uso un fallback simple.
const estadoColor = estado?.es_final
? 'bg-emerald-600'
: 'bg-amber-600'
const estadoColorHex = (estado as any)?.color as
| string
| undefined
return (
<PlanEstudiosCard
@@ -237,7 +237,8 @@ function RouteComponent() {
ciclos={`${plan.numero_ciclos} ${plan.tipo_ciclo.toLowerCase()}s`}
facultad={facultad?.nombre ?? 'Sin Facultad'}
estado={estado?.etiqueta ?? 'Desconocido'}
claseColorEstado={estadoColor}
colorEstadoHex={estadoColorHex}
claseColorEstado={!estadoColorHex ? 'bg-secondary' : ''}
colorFacultad={facultad?.color ?? '#000000'}
onClick={() =>
navigate({