diff --git a/src/components/asignaturas/detalle/AsignaturaDetailPage.tsx b/src/components/asignaturas/detalle/AsignaturaDetailPage.tsx index 9828078..6b5503a 100644 --- a/src/components/asignaturas/detalle/AsignaturaDetailPage.tsx +++ b/src/components/asignaturas/detalle/AsignaturaDetailPage.tsx @@ -1,11 +1,12 @@ import { createFileRoute, useNavigate, useParams } from '@tanstack/react-router' -import { Pencil, Sparkles } from 'lucide-react' -import { useState, useEffect } from 'react' +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, @@ -37,54 +38,15 @@ export interface AsignaturaResponse { datos: AsignaturaDatos } -function isRecord(value: unknown): value is Record { - return typeof value === 'object' && value !== null && !Array.isArray(value) +type CriterioEvaluacionRow = { + label: string + value: number } -function parseContenidoTematicoToPlainText(value: unknown): string { - if (!Array.isArray(value)) return '' - - const blocks: Array = [] - - for (const item of value) { - if (!isRecord(item)) continue - - const unidad = - typeof item.unidad === 'number' && Number.isFinite(item.unidad) - ? item.unidad - : undefined - const titulo = typeof item.titulo === 'string' ? item.titulo : '' - - const header = `${unidad ?? ''}${unidad ? '.' : ''} ${titulo}`.trim() - if (!header) continue - - const lines: Array = [header] - - const temas = Array.isArray(item.temas) ? item.temas : [] - temas.forEach((tema, idx) => { - const temaNombre = - typeof tema === 'string' - ? tema - : isRecord(tema) && typeof tema.nombre === 'string' - ? tema.nombre - : '' - if (!temaNombre) return - - if (unidad != null) { - lines.push(`${unidad}.${idx + 1} ${temaNombre}`.trim()) - } else { - lines.push(`${idx + 1}. ${temaNombre}`) - } - }) - - blocks.push(lines.join('\n')) - } - - return blocks.join('\n\n').trimEnd() -} - -const columnParsers: Partial string>> = { - contenido_tematico: parseContenidoTematicoToPlainText, +type CriterioEvaluacionRowDraft = { + id: string + label: string + value: string // allow empty while editing } export const Route = createFileRoute( @@ -137,6 +99,13 @@ function DatosGenerales({ }) 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 @@ -154,6 +123,56 @@ function DatosGenerales({ 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 ( @@ -213,6 +232,7 @@ function DatosGenerales({ placeholder={placeholder} description={description} onPersist={(clave, value) => onPersistDato(clave, value)} + onOpenEvaluationEditor={openEvaluationEditor} /> ) }, @@ -244,12 +264,12 @@ function DatosGenerales({ @@ -270,6 +290,12 @@ interface InfoCardProps { 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({ @@ -283,10 +309,20 @@ function InfoCard({ 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', @@ -295,11 +331,80 @@ function InfoCard({ useEffect(() => { setData(initialContent) setTempText(initialContent) - }, [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) @@ -325,122 +430,279 @@ function InfoCard({ }) } - return ( - - - -
-
- - - - {title} - - - - {description || 'Información del campo'} - - + 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]) - {required && ( - - * - + 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 + - {!isEditing && ( -
- - - - Mejorar con IA - - - - - - - Editar campo - -
- )} -
- - - - - {isEditing ? ( -
-