Se renderiza el contenido temático en datos generales a partir de su columna en la BDD
This commit is contained in:
@@ -62,6 +62,56 @@ export interface AsignaturaResponse {
|
|||||||
datos: AsignaturaDatos
|
datos: AsignaturaDatos
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||||
|
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseContenidoTematicoToPlainText(value: unknown): string {
|
||||||
|
if (!Array.isArray(value)) return ''
|
||||||
|
|
||||||
|
const blocks: Array<string> = []
|
||||||
|
|
||||||
|
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<string> = [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<Record<string, (value: unknown) => string>> = {
|
||||||
|
contenido_tematico: parseContenidoTematicoToPlainText,
|
||||||
|
}
|
||||||
|
|
||||||
function EditableHeaderField({
|
function EditableHeaderField({
|
||||||
value,
|
value,
|
||||||
onSave,
|
onSave,
|
||||||
@@ -122,8 +172,8 @@ export default function AsignaturaDetailPage() {
|
|||||||
useSubject(asignaturaId)
|
useSubject(asignaturaId)
|
||||||
// 1. Asegúrate de tener estos estados en tu componente principal
|
// 1. Asegúrate de tener estos estados en tu componente principal
|
||||||
const [messages, setMessages] = useState<Array<IAMessage>>([])
|
const [messages, setMessages] = useState<Array<IAMessage>>([])
|
||||||
const [asignatura, setAsignatura] = useState({})
|
const [asignatura, setAsignatura] = useState<AsignaturaDetail | null>(null)
|
||||||
const [campos, setCampos] = useState<Array<CampoEstructura>>([])
|
const [campos] = useState<Array<CampoEstructura>>([])
|
||||||
const [activeTab, setActiveTab] = useState('datos')
|
const [activeTab, setActiveTab] = useState('datos')
|
||||||
const updateAsignatura = useUpdateAsignatura()
|
const updateAsignatura = useUpdateAsignatura()
|
||||||
|
|
||||||
@@ -172,15 +222,12 @@ export default function AsignaturaDetailPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handlePersistDatoGeneral = (clave: string, value: string) => {
|
const handlePersistDatoGeneral = (clave: string, value: string) => {
|
||||||
const baseDatos =
|
const baseDatos = asignatura?.datos ?? (asignaturaApi as any)?.datos ?? {}
|
||||||
(asignatura as any)?.datos ?? (asignaturaApi as any)?.datos ?? {}
|
|
||||||
const mergedDatos = { ...baseDatos, [clave]: value }
|
const mergedDatos = { ...baseDatos, [clave]: value }
|
||||||
|
|
||||||
// Mantener estado local coherente para merges posteriores.
|
// Mantener estado local coherente para merges posteriores.
|
||||||
setAsignatura((prev: any) => ({
|
setAsignatura((prev) => ({
|
||||||
...(prev && Object.keys(prev).length
|
...((prev ?? asignaturaApi ?? {}) as any),
|
||||||
? prev
|
|
||||||
: ((asignaturaApi as any) ?? {})),
|
|
||||||
datos: mergedDatos,
|
datos: mergedDatos,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
@@ -193,9 +240,7 @@ export default function AsignaturaDetailPage() {
|
|||||||
}
|
}
|
||||||
/* ---------- sincronizar API ---------- */
|
/* ---------- sincronizar API ---------- */
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (asignaturaApi?.datos) {
|
if (asignaturaApi) setAsignatura(asignaturaApi)
|
||||||
setAsignatura(asignaturaApi)
|
|
||||||
}
|
|
||||||
}, [asignaturaApi])
|
}, [asignaturaApi])
|
||||||
|
|
||||||
// 2. Funciones de manejo para la IA
|
// 2. Funciones de manejo para la IA
|
||||||
@@ -213,7 +258,7 @@ export default function AsignaturaDetailPage() {
|
|||||||
// toast.info("Enviando consulta a la IA...");
|
// toast.info("Enviando consulta a la IA...");
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleAcceptSuggestion = (sugerencia: IASugerencia) => {
|
const handleAcceptSuggestion = (_sugerencia: IASugerencia) => {
|
||||||
// Lógica para actualizar el valor del campo en tu estado de asignatura
|
// Lógica para actualizar el valor del campo en tu estado de asignatura
|
||||||
// toast.success(`Sugerencia aplicada a ${sugerencia.campoNombre}`);
|
// toast.success(`Sugerencia aplicada a ${sugerencia.campoNombre}`);
|
||||||
}
|
}
|
||||||
@@ -252,7 +297,7 @@ export default function AsignaturaDetailPage() {
|
|||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
{/* ================= HEADER ACTUALIZADO ================= */}
|
{/* ================= HEADER ACTUALIZADO ================= */}
|
||||||
<section className="bg-gradient-to-b from-[#0b1d3a] to-[#0e2a5c] text-white">
|
<section className="bg-linear-to-b from-[#0b1d3a] to-[#0e2a5c] text-white">
|
||||||
<div className="mx-auto max-w-7xl px-6 py-10">
|
<div className="mx-auto max-w-7xl px-6 py-10">
|
||||||
<Link
|
<Link
|
||||||
to="/planes/$planId/asignaturas"
|
to="/planes/$planId/asignaturas"
|
||||||
@@ -284,7 +329,12 @@ export default function AsignaturaDetailPage() {
|
|||||||
<span className="flex items-center gap-1">
|
<span className="flex items-center gap-1">
|
||||||
<GraduationCap className="h-4 w-4 shrink-0" />
|
<GraduationCap className="h-4 w-4 shrink-0" />
|
||||||
<span className="text-blue-100">
|
<span className="text-blue-100">
|
||||||
{asignaturaApi?.planes_estudio?.datos?.nombre || ''}
|
{(() => {
|
||||||
|
const datosPlan = asignaturaApi?.planes_estudio?.datos
|
||||||
|
return isRecord(datosPlan)
|
||||||
|
? String((datosPlan as any).nombre ?? '')
|
||||||
|
: ''
|
||||||
|
})()}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
@@ -357,7 +407,7 @@ export default function AsignaturaDetailPage() {
|
|||||||
{/* ================= TAB: DATOS GENERALES ================= */}
|
{/* ================= TAB: DATOS GENERALES ================= */}
|
||||||
<TabsContent value="datos">
|
<TabsContent value="datos">
|
||||||
<DatosGenerales
|
<DatosGenerales
|
||||||
data={asignatura}
|
data={asignatura ?? asignaturaApi ?? null}
|
||||||
isLoading={loadingAsig}
|
isLoading={loadingAsig}
|
||||||
asignaturaId={asignaturaId}
|
asignaturaId={asignaturaId}
|
||||||
onPersistDato={handlePersistDatoGeneral}
|
onPersistDato={handlePersistDatoGeneral}
|
||||||
@@ -384,12 +434,12 @@ export default function AsignaturaDetailPage() {
|
|||||||
<TabsContent value="ia">
|
<TabsContent value="ia">
|
||||||
<IAAsignaturaTab
|
<IAAsignaturaTab
|
||||||
campos={campos}
|
campos={campos}
|
||||||
asignatura={asignatura}
|
asignatura={(asignatura ?? asignaturaApi ?? {}) as any}
|
||||||
messages={messages}
|
messages={messages}
|
||||||
onSendMessage={handleSendMessage}
|
onSendMessage={handleSendMessage}
|
||||||
onAcceptSuggestion={handleAcceptSuggestion}
|
onAcceptSuggestion={handleAcceptSuggestion}
|
||||||
onRejectSuggestion={
|
onRejectSuggestion={
|
||||||
(id) =>
|
(_id) =>
|
||||||
console.log(
|
console.log(
|
||||||
'Rechazada',
|
'Rechazada',
|
||||||
) /* toast.error("Sugerencia rechazada")*/
|
) /* toast.error("Sugerencia rechazada")*/
|
||||||
@@ -421,7 +471,7 @@ export default function AsignaturaDetailPage() {
|
|||||||
/* ================= TAB CONTENT ================= */
|
/* ================= TAB CONTENT ================= */
|
||||||
interface DatosGeneralesProps {
|
interface DatosGeneralesProps {
|
||||||
asignaturaId: string
|
asignaturaId: string
|
||||||
data: AsignaturaDetail
|
data: AsignaturaDetail | null
|
||||||
isLoading: boolean
|
isLoading: boolean
|
||||||
onPersistDato: (clave: string, value: string) => void
|
onPersistDato: (clave: string, value: string) => void
|
||||||
}
|
}
|
||||||
@@ -431,15 +481,22 @@ function DatosGenerales({
|
|||||||
asignaturaId,
|
asignaturaId,
|
||||||
onPersistDato,
|
onPersistDato,
|
||||||
}: DatosGeneralesProps) {
|
}: DatosGeneralesProps) {
|
||||||
const formatTitle = (key: string): string =>
|
|
||||||
key.replace(/_/g, ' ').replace(/\b\w/g, (l: string) => l.toUpperCase())
|
|
||||||
|
|
||||||
// 1. Extraemos la definición de la estructura (los metadatos)
|
// 1. Extraemos la definición de la estructura (los metadatos)
|
||||||
const structureProps =
|
const definicionRaw = data?.estructuras_asignatura?.definicion
|
||||||
data.estructuras_asignatura?.definicion?.properties || {}
|
const definicion = isRecord(definicionRaw)
|
||||||
|
? (definicionRaw as Record<string, unknown>)
|
||||||
|
: null
|
||||||
|
|
||||||
|
const propertiesRaw = definicion ? (definicion as any).properties : undefined
|
||||||
|
const structureProps = isRecord(propertiesRaw)
|
||||||
|
? (propertiesRaw as Record<string, any>)
|
||||||
|
: {}
|
||||||
|
|
||||||
// 2. Extraemos los valores reales (el contenido redactado)
|
// 2. Extraemos los valores reales (el contenido redactado)
|
||||||
const valoresActuales = data.datos || {}
|
const datosRaw = data?.datos
|
||||||
|
const valoresActuales = isRecord(datosRaw)
|
||||||
|
? (datosRaw as Record<string, any>)
|
||||||
|
: {}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="animate-in fade-in mx-auto max-w-7xl space-y-8 px-4 py-8 duration-500">
|
<div className="animate-in fade-in mx-auto max-w-7xl space-y-8 px-4 py-8 duration-500">
|
||||||
@@ -468,6 +525,11 @@ function DatosGenerales({
|
|||||||
const cardTitle = config.title || key
|
const cardTitle = config.title || key
|
||||||
const description = config.description || ''
|
const description = config.description || ''
|
||||||
|
|
||||||
|
const xColumn =
|
||||||
|
typeof config?.['x-column'] === 'string'
|
||||||
|
? config['x-column']
|
||||||
|
: undefined
|
||||||
|
|
||||||
// Obtenemos el placeholder del arreglo 'examples' de la estructura
|
// Obtenemos el placeholder del arreglo 'examples' de la estructura
|
||||||
const placeholder =
|
const placeholder =
|
||||||
config.examples && config.examples.length > 0
|
config.examples && config.examples.length > 0
|
||||||
@@ -476,11 +538,15 @@ function DatosGenerales({
|
|||||||
|
|
||||||
const valActual = valoresActuales[key]
|
const valActual = valoresActuales[key]
|
||||||
|
|
||||||
const isContentEmpty =
|
let currentContent = valActual ?? ''
|
||||||
!valActual?.description ||
|
|
||||||
valActual.description === config.description
|
|
||||||
|
|
||||||
const currentContent = valActual ?? ''
|
if (xColumn) {
|
||||||
|
const rawValue = (data as any)?.[xColumn]
|
||||||
|
const parser = columnParsers[xColumn]
|
||||||
|
currentContent = parser
|
||||||
|
? parser(rawValue)
|
||||||
|
: String(rawValue ?? '')
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<InfoCard
|
<InfoCard
|
||||||
@@ -489,6 +555,7 @@ function DatosGenerales({
|
|||||||
clave={key}
|
clave={key}
|
||||||
title={cardTitle}
|
title={cardTitle}
|
||||||
initialContent={currentContent} // Si es igual a la descripción de la SEP, pasamos vacío
|
initialContent={currentContent} // Si es igual a la descripción de la SEP, pasamos vacío
|
||||||
|
xColumn={xColumn}
|
||||||
placeholder={placeholder} // Aquí irá "Primer semestre", "MAT-101", etc.
|
placeholder={placeholder} // Aquí irá "Primer semestre", "MAT-101", etc.
|
||||||
description={description} // El texto largo de "Indicar el ciclo..."
|
description={description} // El texto largo de "Indicar el ciclo..."
|
||||||
onEnhanceAI={(contenido) => console.log(contenido)}
|
onEnhanceAI={(contenido) => console.log(contenido)}
|
||||||
@@ -545,6 +612,7 @@ interface InfoCardProps {
|
|||||||
initialContent: any
|
initialContent: any
|
||||||
placeholder?: string
|
placeholder?: string
|
||||||
description?: string
|
description?: string
|
||||||
|
xColumn?: string
|
||||||
required?: boolean // Nueva prop para el asterisco
|
required?: boolean // Nueva prop para el asterisco
|
||||||
type?: 'text' | 'requirements' | 'evaluation'
|
type?: 'text' | 'requirements' | 'evaluation'
|
||||||
onEnhanceAI?: (content: any) => void
|
onEnhanceAI?: (content: any) => void
|
||||||
@@ -558,6 +626,7 @@ function InfoCard({
|
|||||||
initialContent,
|
initialContent,
|
||||||
placeholder,
|
placeholder,
|
||||||
description,
|
description,
|
||||||
|
xColumn,
|
||||||
required,
|
required,
|
||||||
type = 'text',
|
type = 'text',
|
||||||
onPersist,
|
onPersist,
|
||||||
@@ -649,7 +718,20 @@ function InfoCard({
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="h-8 w-8 text-slate-400"
|
className="h-8 w-8 text-slate-400"
|
||||||
onClick={() => setIsEditing(true)}
|
onClick={() => {
|
||||||
|
// Si esta InfoCard proviene de una columna externa (ej: contenido_tematico),
|
||||||
|
// redirigimos a la pestaña de Contenido en vez de editar inline.
|
||||||
|
if (xColumn === 'contenido_tematico') {
|
||||||
|
navigate({
|
||||||
|
to: '/planes/$planId/asignaturas/$asignaturaId',
|
||||||
|
params: { planId, asignaturaId: asignaturaId! },
|
||||||
|
state: { activeTab: 'contenido' } as any,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsEditing(true)
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Pencil className="h-3 w-3" />
|
<Pencil className="h-3 w-3" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -669,7 +751,7 @@ function InfoCard({
|
|||||||
value={tempText}
|
value={tempText}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
onChange={(e) => setTempText(e.target.value)}
|
onChange={(e) => setTempText(e.target.value)}
|
||||||
className="min-h-[120px] text-sm leading-relaxed"
|
className="min-h-30 text-sm leading-relaxed"
|
||||||
/>
|
/>
|
||||||
<div className="flex justify-end gap-2">
|
<div className="flex justify-end gap-2">
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
Reference in New Issue
Block a user