import { createFileRoute, useNavigate, useParams } from '@tanstack/react-router' import { Minus, Pencil, Plus, Sparkles } from 'lucide-react' import { useEffect, useMemo, useRef, useState } from 'react' import type { AsignaturaDetail } from '@/data' import { Button } from '@/components/ui/button' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Input } from '@/components/ui/input' import { Textarea } from '@/components/ui/textarea' import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from '@/components/ui/tooltip' import { useSubject, useUpdateAsignatura } from '@/data/hooks/useSubjects' export interface BibliografiaEntry { id: string tipo: 'BASICA' | 'COMPLEMENTARIA' cita: string fuenteBibliotecaId?: string fuenteBiblioteca?: any } export interface BibliografiaTabProps { id: string bibliografia: Array onSave: (bibliografia: Array) => void isSaving: boolean } export interface AsignaturaDatos { [key: string]: string } export interface AsignaturaResponse { datos: AsignaturaDatos } type CriterioEvaluacionRow = { label: string value: number } type CriterioEvaluacionRowDraft = { id: string label: string value: string // allow empty while editing } export const Route = createFileRoute( '/planes/$planId/asignaturas/$asignaturaId', )({ component: AsignaturaDetailPage, }) export default function AsignaturaDetailPage() { const { asignaturaId } = useParams({ from: '/planes/$planId/asignaturas/$asignaturaId', }) const { data: asignaturaApi } = useSubject(asignaturaId) const [asignatura, setAsignatura] = useState(null) const updateAsignatura = useUpdateAsignatura() const handlePersistDatoGeneral = (clave: string, value: string) => { const baseDatos = asignatura?.datos ?? (asignaturaApi as any)?.datos ?? {} const mergedDatos = { ...baseDatos, [clave]: value } // Mantener estado local coherente para merges posteriores. setAsignatura((prev) => ({ ...((prev ?? asignaturaApi ?? {}) as any), datos: mergedDatos, })) updateAsignatura.mutate({ asignaturaId, patch: { datos: mergedDatos, }, }) } /* ---------- sincronizar API ---------- */ useEffect(() => { if (asignaturaApi) setAsignatura(asignaturaApi) }, [asignaturaApi]) return } function DatosGenerales({ onPersistDato, }: { onPersistDato: (clave: string, value: string) => void }) { const { asignaturaId } = useParams({ from: '/planes/$planId/asignaturas/$asignaturaId', }) const { data: data, isLoading: isLoading } = useSubject(asignaturaId) const updateAsignatura = useUpdateAsignatura() const evaluationCardRef = useRef(null) const [evaluationForceEditToken, setEvaluationForceEditToken] = useState(0) const [evaluationHighlightToken, setEvaluationHighlightToken] = useState(0) // 1. Extraemos la definición de la estructura (los metadatos) const definicionRaw = data?.estructuras_asignatura?.definicion const definicion = isRecord(definicionRaw) ? (definicionRaw as Record) : null const propertiesRaw = definicion ? (definicion as any).properties : undefined const structureProps = isRecord(propertiesRaw) ? (propertiesRaw as Record) : {} // 2. Extraemos los valores reales (el contenido redactado) const datosRaw = data?.datos const valoresActuales = isRecord(datosRaw) ? (datosRaw as Record) : {} const criteriosEvaluacion: Array = useMemo(() => { const raw = (data as any)?.criterios_de_evaluacion console.log(raw) if (!Array.isArray(raw)) return [] const rows: Array = [] for (const item of raw) { if (!isRecord(item)) continue const label = typeof item.label === 'string' ? item.label : '' const valueNum = typeof item.value === 'number' ? item.value : typeof item.value === 'string' ? Number(item.value) : NaN if (!label.trim()) continue if (!Number.isFinite(valueNum)) continue const value = Math.trunc(valueNum) if (value < 1 || value > 100) continue rows.push({ label: label.trim(), value }) } return rows }, [data]) const openEvaluationEditor = () => { evaluationCardRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start', }) const now = Date.now() setEvaluationForceEditToken(now) setEvaluationHighlightToken(now) } const persistCriteriosEvaluacion = async ( rows: Array, ) => { await updateAsignatura.mutateAsync({ asignaturaId: asignaturaId as any, patch: { criterios_de_evaluacion: rows, } as any, }) } if (isLoading) return

Cargando información...

return (
{/* Encabezado de la Sección */}

Datos Generales

Información oficial estructurada bajo los lineamientos de la SEP.

{/* Grid de Información */}
{/* Columna Principal (Más ancha) */}
{Object.entries(structureProps).map( ([key, config]: [string, any]) => { const cardTitle = config.title || key const description = config.description || '' const xColumn = typeof config?.['x-column'] === 'string' ? config['x-column'] : undefined // Obtenemos el placeholder del arreglo 'examples' de la estructura const placeholder = config.examples && config.examples.length > 0 ? config.examples[0] : '' const valActual = valoresActuales[key] let currentContent = valActual ?? '' if (xColumn) { const rawValue = (data as any)?.[xColumn] const parser = columnParsers[xColumn] currentContent = parser ? parser(rawValue) : String(rawValue ?? '') } return ( onPersistDato(clave, value)} onOpenEvaluationEditor={openEvaluationEditor} /> ) }, )}
{/* Columna Lateral (Información Secundaria) */}
{/* Tarjeta de Requisitos */} {/* Tarjeta de Evaluación */}
) } interface InfoCardProps { asignaturaId?: string clave?: string title: string initialContent: any placeholder?: string description?: string xColumn?: string required?: boolean // Nueva prop para el asterisco type?: 'text' | 'requirements' | 'evaluation' onEnhanceAI?: (content: any) => void onPersist?: (clave: string, value: string) => void onPersistEvaluation?: (rows: Array) => Promise onOpenEvaluationEditor?: () => void containerRef?: React.RefObject forceEditToken?: number highlightToken?: number } function InfoCard({ asignaturaId, clave, title, initialContent, placeholder, description, xColumn, required, type = 'text', onPersist, onPersistEvaluation, onOpenEvaluationEditor, containerRef, forceEditToken, highlightToken, }: InfoCardProps) { const [isEditing, setIsEditing] = useState(false) const [isHighlighted, setIsHighlighted] = useState(false) const [data, setData] = useState(initialContent) const [tempText, setTempText] = useState(initialContent) const [evalRows, setEvalRows] = useState>( [], ) const navigate = useNavigate() const { planId } = useParams({ from: '/planes/$planId/asignaturas/$asignaturaId', }) useEffect(() => { setData(initialContent) setTempText(initialContent) if (type === 'evaluation') { const raw = Array.isArray(initialContent) ? initialContent : [] const rows: Array = raw .map((r: any): CriterioEvaluacionRowDraft | null => { const label = typeof r?.label === 'string' ? r.label : '' const valueNum = typeof r?.value === 'number' ? r.value : typeof r?.value === 'string' ? Number(r.value) : NaN const value = Number.isFinite(valueNum) ? String(Math.trunc(valueNum)) : '' return { id: crypto.randomUUID(), label, value, } }) .filter(Boolean) as Array setEvalRows(rows) } }, [initialContent, type]) useEffect(() => { if (!forceEditToken) return setIsEditing(true) }, [forceEditToken]) useEffect(() => { if (!highlightToken) return setIsHighlighted(true) const t = window.setTimeout(() => setIsHighlighted(false), 900) return () => window.clearTimeout(t) }, [highlightToken]) const handleSave = () => { console.log('clave, valor:', clave, String(tempText ?? '')) if (type === 'evaluation') { const cleaned: Array = [] for (const r of evalRows) { const label = String(r.label).trim() const valueStr = String(r.value).trim() if (!label) continue if (!valueStr) continue const n = Number(valueStr) if (!Number.isFinite(n)) continue const value = Math.trunc(n) if (value < 1 || value > 100) continue cleaned.push({ label, value }) } setData(cleaned) setEvalRows( cleaned.map((x) => ({ id: crypto.randomUUID(), label: x.label, value: String(x.value), })), ) setIsEditing(false) void onPersistEvaluation?.(cleaned) return } setData(tempText) setIsEditing(false) if (type === 'text' && clave && onPersist) { onPersist(clave, String(tempText ?? '')) } } const handleIARequest = (campoClave: string) => { console.log(placeholder) // Añadimos un timestamp a la state para forzar que la navegación // genere una nueva ubicación incluso si la ruta y los params son iguales. navigate({ to: '/planes/$planId/asignaturas/$asignaturaId/iaasignatura', params: { planId, asignaturaId: asignaturaId! }, state: { activeTab: 'ia', prefillCampo: campoClave, prefillContenido: data, _ts: Date.now(), } as any, }) } const evaluationTotal = useMemo(() => { if (type !== 'evaluation') return 0 return evalRows.reduce((acc, r) => { const v = String(r.value).trim() if (!v) return acc const n = Number(v) if (!Number.isFinite(n)) return acc const value = Math.trunc(n) if (value < 1 || value > 100) return acc return acc + value }, 0) }, [type, evalRows]) return (
{title} {description || 'Información del campo'} {required && ( * )}
{!isEditing && (
Mejorar con IA Editar campo
)}
{isEditing ? (
{type === 'evaluation' ? (
{evalRows.map((row) => (
{ const nextLabel = e.target.value setEvalRows((prev) => prev.map((r) => r.id === row.id ? { ...r, label: nextLabel } : r, ), ) }} /> { const raw = e.target.value // Solo permitir '' o dígitos if (raw !== '' && !/^\d+$/.test(raw)) return if (raw === '') { setEvalRows((prev) => prev.map((r) => r.id === row.id ? { ...r, value: '' } : r, ), ) return } const n = Number(raw) if (!Number.isFinite(n)) return const value = Math.trunc(n) if (value < 1 || value > 100) return // No permitir suma > 100 setEvalRows((prev) => { const next = prev.map((r) => r.id === row.id ? { ...r, value: raw } : r, ) const total = next.reduce((acc, r) => { const v = String(r.value).trim() if (!v) return acc const nn = Number(v) if (!Number.isFinite(nn)) return acc const vv = Math.trunc(nn) if (vv < 1 || vv > 100) return acc return acc + vv }, 0) return total > 100 ? prev : next }) }} />
))}
Total: {evaluationTotal}/100
) : (