diff --git a/src/routes/planes/$planId/_detalle/mapa.tsx b/src/routes/planes/$planId/_detalle/mapa.tsx index c67083e..f7358ec 100644 --- a/src/routes/planes/$planId/_detalle/mapa.tsx +++ b/src/routes/planes/$planId/_detalle/mapa.tsx @@ -1,7 +1,5 @@ +/* eslint-disable jsx-a11y/label-has-associated-control */ import { createFileRoute } from '@tanstack/react-router' -import { useMemo, useState, useEffect } from 'react' -import { Badge } from '@/components/ui/badge' -import { Input } from '@/components/ui/input' import { Plus, ChevronDown, @@ -9,7 +7,11 @@ import { GripVertical, Trash2, } from 'lucide-react' +import { useMemo, useState, useEffect } from 'react' + import type { Materia, LineaCurricular } from '@/types/plan' + +import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { Dialog, @@ -23,12 +25,20 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu' +import { Input } from '@/components/ui/input' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select' import { usePlanAsignaturas, usePlanLineas } from '@/data' // --- Mapeadores (Fuera del componente para mayor limpieza) --- const mapLineasToLineaCurricular = ( - lineasApi: any[] = [], -): LineaCurricular[] => { + lineasApi: Array = [], +): Array => { return lineasApi.map((linea) => ({ id: linea.id, nombre: linea.nombre, @@ -37,7 +47,7 @@ const mapLineasToLineaCurricular = ( })) } -const mapAsignaturasToMaterias = (asigApi: any[] = []): Materia[] => { +const mapAsignaturasToMaterias = (asigApi: Array = []): Array => { return asigApi.map((asig) => ({ id: asig.id, clave: asig.codigo, @@ -50,6 +60,7 @@ const mapAsignaturasToMaterias = (asigApi: any[] = []): Materia[] => { orden: asig.orden_celda ?? 0, hd: Math.floor((asig.horas_semana ?? 0) / 2), hi: Math.ceil((asig.horas_semana ?? 0) / 2), + prerrequisitos: [], })) } @@ -151,18 +162,55 @@ function MapaCurricularPage() { // 1. Fetch de Datos const { data: asignaturasApi, isLoading: loadingAsig } = usePlanAsignaturas( - /*planId*/ '0e0aea4d-b8b4-4e75-8279-6224c3ac769f', + /* planId*/ '0e0aea4d-b8b4-4e75-8279-6224c3ac769f', ) const { data: lineasApi, isLoading: loadingLineas } = usePlanLineas( - /*planId*/ '0e0aea4d-b8b4-4e75-8279-6224c3ac769f', + /* planId*/ '0e0aea4d-b8b4-4e75-8279-6224c3ac769f', ) // 2. Estado Local (Para interactividad) - const [materias, setMaterias] = useState([]) - const [lineas, setLineas] = useState([]) + const [materias, setMaterias] = useState>([]) + const [lineas, setLineas] = useState>([]) const [draggedMateria, setDraggedMateria] = useState(null) const [isEditModalOpen, setIsEditModalOpen] = useState(false) const [selectedMateria, setSelectedMateria] = useState(null) + const [hasAreaComun, setHasAreaComun] = useState(false) + const [nombreNuevaLinea, setNombreNuevaLinea] = useState('') // Para el input de nombre personalizado + + const manejarAgregarLinea = (nombre: string) => { + const nombreNormalizado = nombre.trim() + + // Validar si es Área Común (insensible a mayúsculas/minúsculas) + const esAreaComun = + nombreNormalizado.toLowerCase() === 'área común' || + nombreNormalizado.toLowerCase() === 'area comun' + + if (esAreaComun && hasAreaComun) { + alert('El Área Común ya ha sido agregada.') + return + } + + const nueva = { + id: crypto.randomUUID(), + nombre: nombreNormalizado, + orden: lineas.length + 1, + } + + setLineas([...lineas, nueva]) + + if (esAreaComun) { + setHasAreaComun(true) + } + setNombreNuevaLinea('') // Limpiar input + } + + const tieneAreaComun = useMemo(() => { + return lineas.some( + (l) => + l.nombre.toLowerCase() === 'área común' || + l.nombre.toLowerCase() === 'area comun', + ) + }, [lineas]) // 3. Sincronizar API -> Estado Local useEffect(() => { @@ -176,6 +224,25 @@ function MapaCurricularPage() { const ciclosTotales = 9 const ciclosArray = Array.from({ length: ciclosTotales }, (_, i) => i + 1) + // Nuevo estado para controlar los datos temporales del modal de edición + const [editingData, setEditingData] = useState(null) + + // 1. FUNCION DE GUARDAR MODAL + const handleSaveChanges = () => { + if (!editingData) return + console.log(materias) + + setMaterias((prev) => + prev.map((m) => (m.id === editingData.id ? { ...editingData } : m)), + ) + setIsEditModalOpen(false) + } + + // 2. MODIFICACIÓN: Zona de soltado siempre visible + // Cambiamos la condición: Mostramos la sección si hay materias sin asignar + // O si simplemente queremos tener el "depósito" disponible. + const unassignedMaterias = materias.filter((m) => m.ciclo === null) + // --- Lógica de Gestión --- const agregarLinea = (nombre: string) => { const nueva = { id: crypto.randomUUID(), nombre, orden: lineas.length + 1 } @@ -287,9 +354,39 @@ function MapaCurricularPage() { - agregarLinea('Nueva Línea')}> - Nueva Línea Curricular - + {!tieneAreaComun && ( + <> + manejarAgregarLinea('Área Común')} + className="font-bold text-teal-700" + > + + Agregar Área Común + +
+ + )} + {/* Input para nombre personalizado */} +
+ +
+ setNombreNuevaLinea(e.target.value)} + placeholder="Ej: Optativas" + className="h-8 text-xs" + /> + +
+
@@ -367,7 +464,7 @@ function MapaCurricularPage() { isDragging={draggedMateria === m.id} onDragStart={handleDragStart} onClick={() => { - setSelectedMateria(m) + setEditingData(m) setIsEditModalOpen(true) }} /> @@ -416,77 +513,214 @@ function MapaCurricularPage() { {/* Materias Sin Asignar */} - {materias.filter((m) => m.ciclo === null).length > 0 && ( -
-
- -

- Materias pendientes ( - {materias.filter((m) => m.ciclo === null).length}) + {/* SECCIÓN DE MATERIAS SIN ASIGNAR (Mejorada para estar siempre disponible) */} +
+
+
+

+ Bandeja de Entrada / Materias sin asignar

+ {unassignedMaterias.length}
-
handleDrop(e, null, null)} - > - {materias - .filter((m) => m.ciclo === null) - .map((m) => ( -
- { - setSelectedMateria(m) - setIsEditModalOpen(true) - }} - /> -
- ))} -
+

+ Arrastra una materia aquí para quitarla del mapa +

- )} + +
handleDrop(e, null, null)} // Limpia ciclo y línea + > + {unassignedMaterias.map((m) => ( +
+ { + setEditingData(m) // Cargamos los datos en el estado de edición + setIsEditModalOpen(true) + }} + /> +
+ ))} + {unassignedMaterias.length === 0 && ( +
+ No hay materias pendientes. Arrastra una materia aquí para + desasignarla. +
+ )} +
+
{/* Modal de Edición */} - + - Editar Materia + + Editar Materia + - {selectedMateria && ( -
-
- - -
-
- - -
-
- - -
-
+ + {/* Verificación de seguridad: solo renderiza si hay datos */} + {editingData ? ( +
+ {/* Fila 1: Clave y Nombre */} +
- - + + + setEditingData({ ...editingData, clave: e.target.value }) + } + />
- - + + + setEditingData({ ...editingData, nombre: e.target.value }) + } + />
+ + {/* Fila 2: Créditos y Horas */} +
+
+ + +
+
+ + +
+
+ + +
+
+ + {/* Fila 3: Ciclo y Línea */} +
+
+ + +
+
+ + +
+
+ + {/* Fila 4: Seriación (Igual a tu imagen) */} +
+ + +
+ {/* Aquí usamos el array vacío que inicializamos en el mapeador */} + {editingData.prerrequisitos.map((pre) => ( + + {pre} × + + ))} +
+
+ + {/* Fila 5: Tipo */} +
+ + +
+ +
+ + +
+ ) : ( +
No hay datos seleccionados
)} -
- - -

diff --git a/src/types/plan.ts b/src/types/plan.ts index b4c8510..a9dc1a2 100644 --- a/src/types/plan.ts +++ b/src/types/plan.ts @@ -1,102 +1,107 @@ -export type PlanStatus = - | 'borrador' - | 'revision' - | 'expertos' - | 'consejo' - | 'aprobado' - | 'rechazado'; +export type PlanStatus = + | 'borrador' + | 'revision' + | 'expertos' + | 'consejo' + | 'aprobado' + | 'rechazado' -export type TipoPlan = 'Licenciatura' | 'Maestría' | 'Doctorado' | 'Especialidad'; +export type TipoPlan = + | 'Licenciatura' + | 'Maestría' + | 'Doctorado' + | 'Especialidad' -export type TipoMateria = 'obligatoria' | 'optativa' | 'troncal'; +export type TipoMateria = 'obligatoria' | 'optativa' | 'troncal' -export type MateriaStatus = 'borrador' | 'revisada' | 'aprobada'; +export type MateriaStatus = 'borrador' | 'revisada' | 'aprobada' export interface Facultad { - id: string; - nombre: string; - color: string; - icono: string; + id: string + nombre: string + color: string + icono: string } export interface Carrera { - id: string; - nombre: string; - facultadId: string; + id: string + nombre: string + facultadId: string } export interface LineaCurricular { - id: string; - nombre: string; - orden: number; - color?: string; + id: string + nombre: string + orden: number + color?: string } export interface Materia { - id: string; - clave: string; - nombre: string; - creditos: number; - ciclo: number | null; - lineaCurricularId: string | null; - tipo: TipoMateria; - estado: MateriaStatus; - orden?: number; - hd: number; // <--- Añadir - hi: number; // <--- Añadir + id: string + clave: string + nombre: string + creditos: number + ciclo: number | null + lineaCurricularId: string | null + tipo: TipoMateria + estado: MateriaStatus + orden?: number + hd: number // <--- Añadir + hi: number // <--- Añadir + prerrequisitos: Array } export interface Plan { - id: string; - nombre: string; - carrera: Carrera; - facultad: Facultad; - tipoPlan: TipoPlan; - nivel?: string; - modalidad?: string; - duracionCiclos: number; - creditosTotales: number; - fechaCreacion: string; - estadoActual: PlanStatus; + id: string + nombre: string + carrera: Carrera + facultad: Facultad + tipoPlan: TipoPlan + nivel?: string + modalidad?: string + duracionCiclos: number + creditosTotales: number + fechaCreacion: string + estadoActual: PlanStatus } export interface DatosGeneralesField { - id: string; - label: string; - value: string; - tipo: 'texto' | 'lista' | 'parrafo'; - requerido: boolean; + id: string + label: string + value: string + tipo: 'texto' | 'lista' | 'parrafo' + requerido: boolean } export interface CambioPlan { - id: string; - fecha: string; - usuario: string; - tab: string; - descripcion: string; - detalle?: string; + id: string + fecha: string + usuario: string + tab: string + descripcion: string + detalle?: string } export interface ComentarioFlujo { - id: string; - usuario: string; - fecha: string; - texto: string; - fase: PlanStatus; + id: string + usuario: string + fecha: string + texto: string + fase: PlanStatus } export interface DocumentoPlan { - id: string; - fechaGeneracion: string; - version: number; - url?: string; + id: string + fechaGeneracion: string + version: number + url?: string } -export type PlanTab = - | 'datos-generales' - | 'mapa-curricular' - | 'materias' - | 'flujo' - | 'ia' - | 'documento' - | 'historial'; +export type PlanTab = + | 'datos-generales' + | 'mapa-curricular' + | 'materias' + | 'flujo' + | 'ia' + | 'documento' + | 'historial'