From bd0fcd5049a8f4db016917f2bd9755202fcecf20 Mon Sep 17 00:00:00 2001 From: "Roberto.silva" Date: Fri, 9 Jan 2026 15:55:58 -0600 Subject: [PATCH 01/12] Se agrega avance de integracion de datos --- src/routes/planes/$planId/_detalle/datos.tsx | 51 ++++++++----- src/routes/planes/$planId/_detalle/mapa.tsx | 76 ++++++++++++++++---- 2 files changed, 97 insertions(+), 30 deletions(-) diff --git a/src/routes/planes/$planId/_detalle/datos.tsx b/src/routes/planes/$planId/_detalle/datos.tsx index d10d268..433db03 100644 --- a/src/routes/planes/$planId/_detalle/datos.tsx +++ b/src/routes/planes/$planId/_detalle/datos.tsx @@ -1,6 +1,6 @@ import { usePlan } from '@/data'; import { createFileRoute } from '@tanstack/react-router' -import { useState } from 'react' +import { useState, useEffect } from 'react' import type { DatosGeneralesField } from '@/types/plan' import { Button } from '@/components/ui/button' import { Textarea } from '@/components/ui/textarea' @@ -17,23 +17,40 @@ export const Route = createFileRoute('/planes/$planId/_detalle/datos')({ component: DatosGeneralesPage, }) -function DatosGeneralesPage() { - const {data, isFetching} = usePlan('0e0aea4d-b8b4-4e75-8279-6224c3ac769f'); - if(!isFetching && !data) { - return
No se encontró el plan de estudios.
- } - console.log(data); - - // 1. Definimos los DATOS iniciales (Lo que antes venía por props) - const [campos, setCampos] = useState([ - { id: '1', label: 'Objetivo General', value: 'Formar profesionales...', requerido: true, tipo: 'texto' }, - { id: '2', label: 'Perfil de Ingreso', value: 'Interés por la tecnología...', requerido: true, tipo: 'lista' }, - { id: '3', label: 'Perfil de Egreso', value: '', requerido: true, tipo: 'texto' }, - ]) +const formatLabel = (key: string) => { + const result = key.replace(/_/g, ' '); + return result.charAt(0).toUpperCase() + result.slice(1); +}; - // 2. Estados de edición - const [editingId, setEditingId] = useState(null) - const [editValue, setEditValue] = useState('') +function DatosGeneralesPage() { + const { data, isFetching } = usePlan('0e0aea4d-b8b4-4e75-8279-6224c3ac769f'); + + // Inicializamos campos como un arreglo vacío + const [campos, setCampos] = useState([]); + const [editingId, setEditingId] = useState(null); + const [editValue, setEditValue] = useState(''); + + + // Efecto para transformar data?.datos en el arreglo de campos + useEffect(() => { + if (data) { + // Si data es directamente el objeto que mostraste, usamos data. + // Si viene dentro de .datos, usamos data.datos. + const sourceData = data?.datos; + + const datosTransformados: DatosGeneralesField[] = Object.entries(sourceData).map( + ([key, value], index) => ({ + id: (index + 1).toString(), // Id basado en index (1, 2, 3...) + label: formatLabel(key), // "perfil_de_ingreso" -> "Perfil de ingreso" + value: value?.toString() || '', // Manejo de nulls + requerido: true, + tipo: 'texto' // Todos como texto según tu instrucción + }) + ); + + setCampos(datosTransformados); + } + }, [data]); // 3. Manejadores de acciones (Ahora como funciones locales) const handleEdit = (campo: DatosGeneralesField) => { diff --git a/src/routes/planes/$planId/_detalle/mapa.tsx b/src/routes/planes/$planId/_detalle/mapa.tsx index 3999584..9954a57 100644 --- a/src/routes/planes/$planId/_detalle/mapa.tsx +++ b/src/routes/planes/$planId/_detalle/mapa.tsx @@ -1,5 +1,5 @@ import { createFileRoute } from '@tanstack/react-router' -import { useState } from 'react' +import { useMemo, useState } from 'react' import { Badge } from '@/components/ui/badge' import { Input } from '@/components/ui/input' import { @@ -20,22 +20,14 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu" +import { usePlanAsignaturas, usePlanLineas } from '@/data'; + export const Route = createFileRoute('/planes/$planId/_detalle/mapa')({ component: MapaCurricularPage, }) -// --- Constantes de Estilo y Datos --- -const INITIAL_LINEAS: LineaCurricular[] = [ - { id: 'l1', nombre: 'Formación Básica', orden: 1 }, - { id: 'l2', nombre: 'Ciencias de la Computación', orden: 2 }, -]; -const INITIAL_MATERIAS: Materia[] = [ - { id: "1", clave: 'MAT101', nombre: 'Cálculo Diferencial', creditos: 8, hd: 4, hi: 4, ciclo: 1, lineaCurricularId: 'l1', tipo: 'obligatoria', estado: 'aprobada' }, - { id: "2", clave: 'FIS101', nombre: 'Física Mecánica', creditos: 6, hd: 3, hi: 3, ciclo: 1, lineaCurricularId: 'l1', tipo: 'obligatoria', estado: 'aprobada' }, - { id: "3", clave: 'PRO101', nombre: 'Fundamentos de Programación', creditos: 8, hd: 4, hi: 4, ciclo: null, lineaCurricularId: null, tipo: 'obligatoria', estado: 'borrador' }, -]; const lineColors = [ 'bg-blue-50 border-blue-200 text-blue-700', @@ -94,6 +86,24 @@ function MateriaCardItem({ materia, onDragStart, isDragging, onClick }: { // --- Componente Principal --- function MapaCurricularPage() { + + + const { data: asignaturas, isFetching: loadingAsig } = usePlanAsignaturas('0e0aea4d-b8b4-4e75-8279-6224c3ac769f'); + const { data: lineas2, isFetching: loadingLineas } = usePlanLineas('0e0aea4d-b8b4-4e75-8279-6224c3ac769f'); + console.log(asignaturas); + console.log(lineas2); + // --- Constantes de Estilo y Datos --- +const INITIAL_LINEAS: LineaCurricular[] = [ + { id: 'l1', nombre: 'Formación Básica', orden: 1 }, + { id: 'l2', nombre: 'Ciencias de la Computación', orden: 2 }, +]; + +const INITIAL_MATERIAS: Materia[] = [ + { id: "1", clave: 'MAT101', nombre: 'Cálculo Diferencial', creditos: 8, hd: 4, hi: 4, ciclo: 1, lineaCurricularId: 'l1', tipo: 'obligatoria', estado: 'aprobada' }, + { id: "2", clave: 'FIS101', nombre: 'Física Mecánica', creditos: 6, hd: 3, hi: 3, ciclo: 1, lineaCurricularId: 'l1', tipo: 'obligatoria', estado: 'aprobada' }, + { id: "3", clave: 'PRO101', nombre: 'Fundamentos de Programación', creditos: 8, hd: 4, hi: 4, ciclo: null, lineaCurricularId: null, tipo: 'obligatoria', estado: 'borrador' }, +]; + const [materias, setMaterias] = useState(INITIAL_MATERIAS); const [lineas, setLineas] = useState(INITIAL_LINEAS); const [draggedMateria, setDraggedMateria] = useState(null); @@ -103,6 +113,46 @@ function MapaCurricularPage() { const ciclosTotales = 9; const ciclosArray = Array.from({ length: ciclosTotales }, (_, i) => i + 1); + const mapLineasToLineaCurricular = (lineasApi = []): LineaCurricular[] => { + return lineasApi.map((linea: any) => ({ + id: linea.id, + nombre: linea.nombre, + orden: linea.orden ?? 0, + color: '#1976d2', // default aceptado + })); +}; + +const mapAsignaturasToMaterias = (asigApi = []): Materia[] => { + return asigApi.map((asig: any) => ({ + id: asig.id, + clave: asig.codigo, + nombre: asig.nombre, + creditos: asig.creditos ?? 0, + ciclo: asig.numero_ciclo ?? null, + lineaCurricularId: asig.linea_plan_id ?? null, + tipo: asig.tipo === 'OBLIGATORIA' ? 'obligatoria' : 'optativa', + estado: 'borrador', // default válido + orden: asig.orden_celda ?? 0, + hd: Math.floor((asig.horas_semana ?? 0) / 2), + hi: Math.ceil((asig.horas_semana ?? 0) / 2), + })); +}; + +const lineasFinales: LineaCurricular[] = useMemo(() => { + return [ + ...INITIAL_LINEAS, + ...mapLineasToLineaCurricular(lineas2), + ]; +}, [lineas2]); + +const materiasFinales: Materia[] = useMemo(() => { + return [ + ...INITIAL_MATERIAS, + ...mapAsignaturasToMaterias(asignaturas), + ]; +}, [asignaturas]); + + // --- Lógica de Gestión --- const agregarLinea = (nombre: string) => { const nueva = { id: crypto.randomUUID(), nombre, orden: lineas.length + 1 }; @@ -192,7 +242,7 @@ function MapaCurricularPage() { {/* Filas por Línea */} - {lineas.map((linea, idx) => { + {lineasFinales.map((linea, idx) => { const sub = getSubtotalLinea(linea.id); return (
@@ -208,7 +258,7 @@ function MapaCurricularPage() { onDrop={(e) => handleDrop(e, ciclo, linea.id)} className="min-h-[140px] p-2 rounded-xl border-2 border-dashed border-slate-100 bg-slate-50/20 space-y-2" > - {materias.filter(m => m.ciclo === ciclo && m.lineaCurricularId === linea.id).map(m => ( + {materiasFinales.filter(m => m.ciclo === ciclo && m.lineaCurricularId === linea.id).map(m => ( { setSelectedMateria(m); setIsEditModalOpen(true); }} /> ))}
From c4329785cc7d6b7a01c1481b817224085d94ad6d Mon Sep 17 00:00:00 2001 From: "Roberto.silva" Date: Tue, 13 Jan 2026 16:28:13 -0600 Subject: [PATCH 02/12] Se llenan datos de las tabs de plan de estudios detalle ( datos, mapa, materia) y se agrega peticion de materia detalle en asignaturas --- .../asignaturas/detalle/MateriaDetailPage.tsx | 370 ++++++----- src/routes/planes/$planId/_detalle/datos.tsx | 125 ++-- src/routes/planes/$planId/_detalle/iaplan.tsx | 327 +++++++--- src/routes/planes/$planId/_detalle/mapa.tsx | 614 +++++++++++------- .../planes/$planId/_detalle/materias.tsx | 385 ++++++----- 5 files changed, 1114 insertions(+), 707 deletions(-) diff --git a/src/components/asignaturas/detalle/MateriaDetailPage.tsx b/src/components/asignaturas/detalle/MateriaDetailPage.tsx index 3a7cd71..5516e2b 100644 --- a/src/components/asignaturas/detalle/MateriaDetailPage.tsx +++ b/src/components/asignaturas/detalle/MateriaDetailPage.tsx @@ -1,123 +1,117 @@ - -import { useCallback, useState } from 'react' -import { Link } from '@tanstack/react-router' +import { useCallback, useState } from 'react' +import { Link } from '@tanstack/react-router' import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { Separator } from '@/components/ui/separator' import { Textarea } from '@/components/ui/textarea' -import { - ArrowLeft, - GraduationCap, - Edit2, Save, - Pencil -} from 'lucide-react' +import { ArrowLeft, GraduationCap, Edit2, Save, Pencil } from 'lucide-react' import { ContenidoTematico } from './ContenidoTematico' import { BibliographyItem } from './BibliographyItem' import { IAMateriaTab } from './IAMateriaTab' -import type { +import type { CampoEstructura, - IAMessage, - IASugerencia, - UnidadTematica, -} from '@/types/materia'; + IAMessage, + IASugerencia, + UnidadTematica, +} from '@/types/materia' import { mockMateria, mockEstructura, mockDocumentoSep, - mockHistorial -} from '@/data/mockMateriaData'; + mockHistorial, +} from '@/data/mockMateriaData' import { DocumentoSEPTab } from './DocumentoSEPTab' import { HistorialTab } from './HistorialTab' +import { useSubject } from '@/data/hooks/useSubjects' export interface BibliografiaEntry { - id: string; - tipo: 'BASICA' | 'COMPLEMENTARIA'; - cita: string; - fuenteBibliotecaId?: string; - fuenteBiblioteca?: any; + id: string + tipo: 'BASICA' | 'COMPLEMENTARIA' + cita: string + fuenteBibliotecaId?: string + fuenteBiblioteca?: any } export interface BibliografiaTabProps { - bibliografia: BibliografiaEntry[]; - onSave: (bibliografia: BibliografiaEntry[]) => void; - isSaving: boolean; + bibliografia: BibliografiaEntry[] + onSave: (bibliografia: BibliografiaEntry[]) => void + isSaving: boolean } export default function MateriaDetailPage() { - // 1. Asegúrate de tener estos estados en tu componente principal -const [messages, setMessages] = useState([]); -const [datosGenerales, setDatosGenerales] = useState({}); -const [campos, setCampos] = useState([]); + const [messages, setMessages] = useState([]) + const [datosGenerales, setDatosGenerales] = useState({}) + const [campos, setCampos] = useState([]) -// 2. Funciones de manejo para la IA -const handleSendMessage = (text: string, campoId?: string) => { - const newMessage: IAMessage = { - id: Date.now().toString(), - role: 'user', - content: text, - timestamp: new Date(), - campoAfectado: campoId - }; - setMessages([...messages, newMessage]); - - // Aquí llamarías a tu API de OpenAI/Claude - //toast.info("Enviando consulta a la IA..."); -}; + // 2. Funciones de manejo para la IA + const handleSendMessage = (text: string, campoId?: string) => { + const newMessage: IAMessage = { + id: Date.now().toString(), + role: 'user', + content: text, + timestamp: new Date(), + campoAfectado: campoId, + } + setMessages([...messages, newMessage]) -const handleAcceptSuggestion = (sugerencia: IASugerencia) => { - // Lógica para actualizar el valor del campo en tu estado de datosGenerales - //toast.success(`Sugerencia aplicada a ${sugerencia.campoNombre}`); -}; + // Aquí llamarías a tu API de OpenAI/Claude + //toast.info("Enviando consulta a la IA..."); + } + + const handleAcceptSuggestion = (sugerencia: IASugerencia) => { + // Lógica para actualizar el valor del campo en tu estado de datosGenerales + //toast.success(`Sugerencia aplicada a ${sugerencia.campoNombre}`); + } // Dentro de tu componente principal (donde están los Tabs) -const [bibliografia, setBibliografia] = useState([ - { - id: '1', - tipo: 'BASICA', - cita: 'Russell, S., & Norvig, P. (2020). Artificial Intelligence: A Modern Approach. Pearson.' - } -]); -const [isSaving, setIsSaving] = useState(false); + const [bibliografia, setBibliografia] = useState([ + { + id: '1', + tipo: 'BASICA', + cita: 'Russell, S., & Norvig, P. (2020). Artificial Intelligence: A Modern Approach. Pearson.', + }, + ]) + const [isSaving, setIsSaving] = useState(false) -const handleSaveBibliografia = (data: BibliografiaEntry[]) => { - setIsSaving(true); - // Aquí iría tu llamada a la API - setBibliografia(data); - - // Simulamos un guardado - setTimeout(() => { - setIsSaving(false); - //toast.success("Cambios guardados"); - }, 1000); -}; + const handleSaveBibliografia = (data: BibliografiaEntry[]) => { + setIsSaving(true) + // Aquí iría tu llamada a la API + setBibliografia(data) - const [isRegenerating, setIsRegenerating] = useState(false); - -const handleRegenerateDocument = useCallback(() => { - setIsRegenerating(true); + // Simulamos un guardado setTimeout(() => { - setIsRegenerating(false); - }, 2000); - }, []); + setIsSaving(false) + //toast.success("Cambios guardados"); + }, 1000) + } + + const [isRegenerating, setIsRegenerating] = useState(false) + + const handleRegenerateDocument = useCallback(() => { + setIsRegenerating(true) + setTimeout(() => { + setIsRegenerating(false) + }, 2000) + }, []) return (
{/* ================= HEADER ================= */}
-
+
- + Volver al plan
- + IA-401 @@ -127,7 +121,7 @@ const handleRegenerateDocument = useCallback(() => {
- + Ingeniería en Sistemas Computacionales @@ -136,13 +130,13 @@ const handleRegenerateDocument = useCallback(() => {

Pertenece al plan:{' '} - + Licenciatura en Ingeniería en Sistemas Computacionales 2024

-
+
8 créditos 7° semestre Sistemas Inteligentes @@ -152,10 +146,10 @@ const handleRegenerateDocument = useCallback(() => {
{/* ================= TABS ================= */} -
-
+
+
- + Datos generales Contenido temático Bibliografía @@ -176,22 +170,27 @@ const handleRegenerateDocument = useCallback(() => { - - console.log("Rechazada") /*toast.error("Sugerencia rechazada")*/} - /> + + console.log( + 'Rechazada', + ) /*toast.error("Sugerencia rechazada")*/ + } + /> @@ -206,7 +205,7 @@ const handleRegenerateDocument = useCallback(() => { - +
@@ -218,79 +217,93 @@ const handleRegenerateDocument = useCallback(() => { /* ================= TAB CONTENT ================= */ function DatosGenerales() { + const { data: asignaturasApi, isLoading: loadingAsig } = useSubject( + /*planId*/ '9d4dda6a-488f-428a-8a07-38081592a641', + ) + + console.log(asignaturasApi.datos) + return ( -
- +
{/* Encabezado de la Sección */} -
+
-

Datos Generales

-

+

+ Datos Generales +

+

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

{/* Grid de Información */} -
- +
{/* Columna Principal (Más ancha) */} -
-
- - - -
- -
- -
+
+
+ + + +
+ +
+ +
{/* Columna Lateral (Información Secundaria) */}
- {/* Tarjeta de Requisitos */} - + {/* Tarjeta de Requisitos */} + - {/* Tarjeta de Evaluación */} - -
+ {/* Tarjeta de Evaluación */} + +
@@ -298,9 +311,9 @@ function DatosGenerales() { } interface InfoCardProps { - title: string, + title: string subtitle?: string - isList?:boolean + isList?: boolean initialContent: any // Puede ser string o array de objetos type?: 'text' | 'list' | 'requirements' | 'evaluation' } @@ -310,23 +323,30 @@ function InfoCard({ title, initialContent, type = 'text' }: InfoCardProps) { const [data, setData] = useState(initialContent) // Estado temporal para el área de texto (siempre editamos como texto por simplicidad) const [tempText, setTempText] = useState( - type === 'text' || type === 'list' - ? initialContent - : JSON.stringify(initialContent, null, 2) // O un formato legible + type === 'text' || type === 'list' + ? initialContent + : JSON.stringify(initialContent, null, 2), // O un formato legible ) const handleSave = () => { // Aquí podrías parsear el texto de vuelta si es necesario - setData(tempText) + setData(tempText) setIsEditing(false) } return ( - - {title} + + + {title} + {!isEditing && ( - )} @@ -335,14 +355,22 @@ function InfoCard({ title, initialContent, type = 'text' }: InfoCardProps) { {isEditing ? (
-