diff --git a/src/components/asignaturas/detalle/AsignaturaDetailPage.tsx b/src/components/asignaturas/detalle/AsignaturaDetailPage.tsx index d7f46ac..2437195 100644 --- a/src/components/asignaturas/detalle/AsignaturaDetailPage.tsx +++ b/src/components/asignaturas/detalle/AsignaturaDetailPage.tsx @@ -1,19 +1,12 @@ import { createFileRoute, - Link, useNavigate, useParams, useRouterState, } from '@tanstack/react-router' -import { ArrowLeft, GraduationCap, Pencil, Sparkles } from 'lucide-react' +import { Pencil, Sparkles } from 'lucide-react' import { useCallback, useState, useEffect } from 'react' -import { BibliographyItem } from './BibliographyItem' -import { ContenidoTematico } from './ContenidoTematico' -import { DocumentoSEPTab } from './DocumentoSEPTab' -import { HistorialTab } from './HistorialTab' -import { IAAsignaturaTab } from './IAAsignaturaTab' - import type { AsignaturaDetail } from '@/data' import type { CampoEstructura, @@ -21,11 +14,8 @@ import type { IASugerencia, } from '@/types/asignatura' -import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' -import { Separator } from '@/components/ui/separator' -import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs' import { Textarea } from '@/components/ui/textarea' import { Tooltip, @@ -34,11 +24,6 @@ import { TooltipTrigger, } from '@/components/ui/tooltip' import { useSubject, useUpdateAsignatura } from '@/data/hooks/useSubjects' -import { - mockAsignatura, - mockEstructura, - mockDocumentoSep, -} from '@/data/mockAsignaturaData' export interface BibliografiaEntry { id: string @@ -249,176 +234,19 @@ export default function AsignaturaDetailPage() { }, 2000) }, []) - return ( -
- {/* ================= HEADER ACTUALIZADO ================= */} -
-
- - Volver al plan - - -
-
- {/* CÓDIGO EDITABLE */} - - handleUpdateHeader('codigo', val)} - /> - - - {/* NOMBRE EDITABLE */} -

- handleUpdateHeader('nombre', val)} - /> -

- -
- - - - {asignaturaApi?.planes_estudio?.datos?.nombre || ''} - - - - - - {asignaturaApi?.planes_estudio?.carreras?.facultades - ?.nombre || ''} - - -
- -

- Pertenece al plan:{' '} - - {asignaturaApi?.planes_estudio?.nombre} - -

-
- -
- {/* CRÉDITOS EDITABLES */} - - - - handleUpdateHeader('creditos', parseInt(val) || 0) - } - /> - - créditos - - - {/* SEMESTRE EDITABLE */} - - - handleUpdateHeader('ciclo', parseInt(val) || 0) - } - /> - ° ciclo - - - {asignaturaApi?.tipo} -
-
-
-
- - {/* ================= TABS ================= */} -
-
- - - Datos generales - Contenido temático - Bibliografía - IA de la asignatura - Documento SEP - Historial - - - - - {/* ================= TAB: DATOS GENERALES ================= */} - - - - - - - - - - - - - - - console.log( - 'Rechazada', - ) /* toast.error("Sugerencia rechazada")*/ - } - /> - - - - - - - - - - -
-
-
- ) + return } -/* ================= TAB CONTENT ================= */ +interface EstructuraDefinicion { + properties?: Record< + string, + { + title?: string + description?: string + examples?: Array + } + > +} interface DatosGeneralesProps { asignaturaId: string data: AsignaturaDetail @@ -426,20 +254,21 @@ interface DatosGeneralesProps { onPersistDato: (clave: string, value: string) => void } function DatosGenerales({ - data, - isLoading, - asignaturaId, onPersistDato, -}: DatosGeneralesProps) { - const formatTitle = (key: string): string => - key.replace(/_/g, ' ').replace(/\b\w/g, (l: string) => l.toUpperCase()) +}: { + onPersistDato: (clave: string, value: string) => void +}) { + const { asignaturaId } = useParams({ + from: '/planes/$planId/asignaturas/$asignaturaId', + }) + + const { data: data, isLoading: isLoading } = useSubject(asignaturaId) - // 1. Extraemos la definición de la estructura (los metadatos) const structureProps = - data.estructuras_asignatura?.definicion?.properties || {} - - // 2. Extraemos los valores reales (el contenido redactado) - const valoresActuales = data.datos || {} + (data?.estructuras_asignatura?.definicion as EstructuraDefinicion) + .properties || {} + const valoresActuales = data?.datos || {} + if (isLoading) return

Cargando información...

return (
@@ -459,44 +288,33 @@ function DatosGenerales({
{/* Columna Principal (Más ancha) */}
- {isLoading &&

Cargando información...

} + {Object.entries(structureProps).map( + ([key, config]: [string, any]) => { + const cardTitle = config.title || key + const description = config.description || '' - {!isLoading && - Object.entries(structureProps).map( - ([key, config]: [string, any]) => { - // 1. METADATOS (Vienen de structureProps -> config) - const cardTitle = config.title || key - const description = config.description || '' + const placeholder = + config.examples && config.examples.length > 0 + ? config.examples[0] + : '' - // Obtenemos el placeholder del arreglo 'examples' de la estructura - const placeholder = - config.examples && config.examples.length > 0 - ? config.examples[0] - : '' + const valActual = (valoresActuales as Record)[key] + const currentContent = valActual ?? '' - const valActual = valoresActuales[key] - - const isContentEmpty = - !valActual?.description || - valActual.description === config.description - - const currentContent = valActual ?? '' - - return ( - console.log(contenido)} - onPersist={(clave, value) => onPersistDato(clave, value)} - /> - ) - }, - )} + return ( + onPersistDato(clave, value)} + /> + ) + }, + )}
{/* Columna Lateral (Información Secundaria) */} @@ -590,7 +408,7 @@ function InfoCard({ console.log(placeholder) navigate({ - to: '/planes/$planId/asignaturas/$asignaturaId', + to: '/planes/$planId/asignaturas/$asignaturaId/iaasignatura', params: { planId, asignaturaId: asignaturaId! }, state: { activeTab: 'ia', diff --git a/src/components/asignaturas/detalle/BibliographyItem.tsx b/src/components/asignaturas/detalle/BibliographyItem.tsx index 87e8569..acec679 100644 --- a/src/components/asignaturas/detalle/BibliographyItem.tsx +++ b/src/components/asignaturas/detalle/BibliographyItem.tsx @@ -1,3 +1,4 @@ +import { useParams } from '@tanstack/react-router' import { Plus, Search, BookOpen, Trash2, Library, Edit3 } from 'lucide-react' import { useEffect, useState } from 'react' @@ -44,23 +45,13 @@ export interface BibliografiaEntry { fuenteBiblioteca?: any } -interface BibliografiaTabProps { - id: string - bibliografia: Array - onSave: (bibliografia: Array) => void - isSaving: boolean -} - -export function BibliographyItem({ - bibliografia, - id, - onSave, - isSaving, -}: BibliografiaTabProps) { - console.log(id) +export function BibliographyItem() { + const { asignaturaId } = useParams({ + from: '/planes/$planId/asignaturas/$asignaturaId', + }) const { data: bibliografia2, isLoading: loadinasignatura } = - useSubjectBibliografia(id) + useSubjectBibliografia(asignaturaId) const [entries, setEntries] = useState>([]) const [isAddDialogOpen, setIsAddDialogOpen] = useState(false) const [isLibraryDialogOpen, setIsLibraryDialogOpen] = useState(false) @@ -76,7 +67,7 @@ export function BibliographyItem({ if (bibliografia2 && Array.isArray(bibliografia2)) { setEntries(bibliografia2) } - }, [bibliografia2, bibliografia]) + }, [bibliografia2]) const basicaEntries = entries.filter((e) => e.tipo === 'BASICA') const complementariaEntries = entries.filter( diff --git a/src/components/asignaturas/detalle/ContenidoTematico.tsx b/src/components/asignaturas/detalle/ContenidoTematico.tsx index 1bafc4d..e324a73 100644 --- a/src/components/asignaturas/detalle/ContenidoTematico.tsx +++ b/src/components/asignaturas/detalle/ContenidoTematico.tsx @@ -1,3 +1,4 @@ +import { useParams } from '@tanstack/react-router' import { Plus, GripVertical, @@ -30,7 +31,7 @@ import { CollapsibleTrigger, } from '@/components/ui/collapsible' import { Input } from '@/components/ui/input' -import { useUpdateSubjectContenido } from '@/data/hooks/useSubjects' +import { useSubject, useUpdateSubjectContenido } from '@/data/hooks/useSubjects' import { cn } from '@/lib/utils' // import { toast } from 'sonner'; @@ -156,20 +157,14 @@ function serializeUnidadesToApi( } // Props del componente -interface ContenidoTematicoProps { - asignaturaId: string - data?: { - contenido_tematico?: unknown - } | null - isLoading: boolean -} -export function ContenidoTematico({ - asignaturaId, - data, - isLoading, -}: ContenidoTematicoProps) { - const updateContenido = useUpdateSubjectContenido() +export function ContenidoTematico() { + const updateContenido = useUpdateSubjectContenido() + const { asignaturaId } = useParams({ + from: '/planes/$planId/asignaturas/$asignaturaId', + }) + + const { data: data, isLoading: isLoading } = useSubject(asignaturaId) const [unidades, setUnidades] = useState>([]) const [expandedUnits, setExpandedUnits] = useState>(new Set()) const [deleteDialog, setDeleteDialog] = useState<{ diff --git a/src/components/asignaturas/detalle/DocumentoSEPTab.tsx b/src/components/asignaturas/detalle/DocumentoSEPTab.tsx index 771f41c..9349a12 100644 --- a/src/components/asignaturas/detalle/DocumentoSEPTab.tsx +++ b/src/components/asignaturas/detalle/DocumentoSEPTab.tsx @@ -1,19 +1,6 @@ -import { - FileText, - Download, - RefreshCw, - FileCheck, - AlertTriangle, - Loader2, -} from 'lucide-react' +import { FileCheck, Download, RefreshCw, Loader2 } from 'lucide-react' import { useState } from 'react' -import type { - DocumentoAsignatura, - Asignatura, - AsignaturaStructure, -} from '@/types/asignatura' - import { AlertDialog, AlertDialogAction, @@ -25,51 +12,34 @@ import { AlertDialogTitle, AlertDialogTrigger, } from '@/components/ui/alert-dialog' -import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' -import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' -import { cn } from '@/lib/utils' -// import { toast } from 'sonner'; -// import { format } from 'date-fns'; -// import { es } from 'date-fns/locale'; +import { Card } from '@/components/ui/card' interface DocumentoSEPTabProps { - documento: DocumentoAsignatura | null - asignatura: Asignatura - estructura: AsignaturaStructure - datosGenerales: Record + pdfUrl: string | null + isLoading: boolean + onDownload: () => void onRegenerate: () => void isRegenerating: boolean } export function DocumentoSEPTab({ - documento, - asignatura, - datosGenerales, - estructura, + pdfUrl, + isLoading, + onDownload, onRegenerate, isRegenerating, }: DocumentoSEPTabProps) { const [showConfirmDialog, setShowConfirmDialog] = useState(false) - // Check completeness - const camposObligatorios = estructura.campos.filter((c) => c.obligatorio) - const camposCompletos = camposObligatorios.filter((c) => - datosGenerales[c.id]?.trim(), - ) - const completeness = Math.round( - (camposCompletos.length / camposObligatorios.length) * 100, - ) - const isComplete = completeness === 100 - const handleRegenerate = () => { setShowConfirmDialog(false) onRegenerate() - // toast.success('Regenerando documento...'); } return (
+ {/* Header */}

@@ -77,30 +47,24 @@ export function DocumentoSEPTab({ Documento SEP

- Previsualización del documento oficial para la SEP + Previsualización del documento oficial generado

+
- {documento?.estado === 'listo' && ( - )} + - + ¿Regenerar documento SEP? - Se creará una nueva versión del documento con los datos - actuales de la asignatura. La versión anterior quedará en el - historial. + Se generará una nueva versión del documento con la información + actual. + Cancelar @@ -129,308 +94,24 @@ export function DocumentoSEPTab({
-
- {/* Document preview */} -
- - {documento?.estado === 'listo' ? ( -
- {/* Simulated document header */} -
-
-
- - - Programa de Estudios - {asignatura.clave} - -
- Versión {documento.version} -
-
- - {/* Document content simulation */} -
-
- {/* Header */} -
-

- Secretaría de Educación Pública -

-

- {asignatura.nombre} -

-

- Clave: {asignatura.clave} | Créditos:{' '} - {asignatura.creditos || 'N/A'} -

-
- - {/* Datos de la institución */} -
-

- Carrera: {asignatura.carrera} -

-

- Facultad: {asignatura.facultad} -

-

- Plan de estudios:{' '} - {asignatura.planNombre} -

- {asignatura.ciclo && ( -

- Ciclo: {asignatura.ciclo} -

- )} -
- - {/* Campos del documento */} - {estructura.campos.map((campo) => { - const valor = datosGenerales[campo.id] - if (!valor) return null - return ( -
-

- {campo.nombre} -

-

- {valor} -

-
- ) - })} - - {/* Footer */} -
-

- Documento generado el{' '} - {/* format(documento.fechaGeneracion, "d 'de' MMMM 'de' yyyy", { locale: es })*/} -

-

Universidad La Salle

-
-
-
-
- ) : documento?.estado === 'generando' ? ( -
-
- -

- Generando documento... -

-
-
- ) : ( -
-
- -

- No hay documento generado aún -

- {!isComplete && ( -
- - Completa todos los campos obligatorios para generar el - documento -
- )} -
-
- )} -
-
- - {/* Info sidebar */} -
- {/* Status */} - - - - Estado del documento - - - - {documento && ( - <> -
- - Versión - - {documento.version} -
-
- - Generado - - - {/* format(documento.fechaGeneracion, "d MMM yyyy, HH:mm", { locale: es })*/} - -
-
- - Estado - - - {documento.estado === 'listo' && 'Listo'} - {documento.estado === 'generando' && 'Generando'} - {documento.estado === 'error' && 'Error'} - -
- - )} -
-
- - {/* Completeness */} - - - - Completitud de datos - - - -
-
- - Campos obligatorios - - - {camposCompletos.length}/{camposObligatorios.length} - -
-
-
-
-

- {completeness === 100 - ? 'Todos los campos obligatorios están completos' - : `Faltan ${camposObligatorios.length - camposCompletos.length} campos por completar`} -

-
- - {/* Missing fields */} - {!isComplete && ( -
-

- Campos faltantes: -

- {camposObligatorios - .filter((c) => !datosGenerales[c.id]?.trim()) - .map((campo) => ( -
- - {campo.nombre} -
- ))} -
- )} - - - - {/* Requirements */} - - - - Requisitos SEP - - - -
    -
  • -
    - {datosGenerales['objetivo_general'] && ( - - )} -
    - - Objetivo general definido - -
  • -
  • -
    - {datosGenerales['competencias'] && ( - - )} -
    - - Competencias especificadas - -
  • -
  • -
    - {datosGenerales['evaluacion'] && ( - - )} -
    - - Criterios de evaluación - -
  • -
-
-
-
-
+ {/* PDF Preview */} + + {isLoading ? ( +
+ +
+ ) : pdfUrl ? ( +