From 1bd37c7e27e7a285f0c21dabb78cb0137bbae3c2 Mon Sep 17 00:00:00 2001 From: Alejandro Rosales Date: Tue, 24 Mar 2026 08:45:59 -0600 Subject: [PATCH 1/2] Refactor MapaCurricularPage: update state variables and enhance UI components for better clarity and usability --- .../planes/$planId/_detalle/asignaturas.tsx | 2 +- src/routes/planes/$planId/_detalle/mapa.tsx | 295 +++++++++--------- 2 files changed, 145 insertions(+), 152 deletions(-) diff --git a/src/routes/planes/$planId/_detalle/asignaturas.tsx b/src/routes/planes/$planId/_detalle/asignaturas.tsx index f4df6a9..8de9d88 100644 --- a/src/routes/planes/$planId/_detalle/asignaturas.tsx +++ b/src/routes/planes/$planId/_detalle/asignaturas.tsx @@ -73,7 +73,7 @@ const mapAsignaturas = ( estado: asig.estado, hd: asig.horas_academicas ?? 0, hi: asig.horas_independientes ?? 0, - prerrequisitos: [], + prerrequisito_asignatura_id: asig.prerrequisito_asignatura_id ?? null, })) } diff --git a/src/routes/planes/$planId/_detalle/mapa.tsx b/src/routes/planes/$planId/_detalle/mapa.tsx index 7057849..de321f1 100644 --- a/src/routes/planes/$planId/_detalle/mapa.tsx +++ b/src/routes/planes/$planId/_detalle/mapa.tsx @@ -1,7 +1,7 @@ /* eslint-disable jsx-a11y/no-static-element-interactions */ /* eslint-disable jsx-a11y/label-has-associated-control */ import { createFileRoute } from '@tanstack/react-router' -import { Plus, ChevronDown, AlertTriangle, Trash2, Pencil } from 'lucide-react' +import { Plus, AlertTriangle, Trash2, Pencil, Download } from 'lucide-react' import * as Icons from 'lucide-react' import { useMemo, useState, useEffect, Fragment } from 'react' @@ -17,12 +17,6 @@ import { DialogHeader, DialogTitle, } from '@/components/ui/dialog' -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from '@/components/ui/dropdown-menu' import { Input } from '@/components/ui/input' import { Select, @@ -92,19 +86,6 @@ const mapAsignaturasToAsignaturas = ( }) } -const lineColors = [ - 'bg-blue-50 border-blue-200 text-blue-700', - 'bg-purple-50 border-purple-200 text-purple-700', - 'bg-orange-50 border-orange-200 text-orange-700', - 'bg-emerald-50 border-emerald-200 text-emerald-700', -] - -const statusBadge: Record = { - borrador: 'bg-slate-100 text-slate-600', - revisada: 'bg-amber-100 text-amber-700', - aprobada: 'bg-emerald-100 text-emerald-700', -} - // --- Subcomponentes --- function StatItem({ label, @@ -116,14 +97,16 @@ function StatItem({ total?: number }) { return ( -
- - {label}: +
+ + {label} - + {value} {total ? ( - /{total} + + /{total} + ) : ( '' )} @@ -348,7 +331,7 @@ export const Route = createFileRoute('/planes/$planId/_detalle/mapa')({ function MapaCurricularPage() { const { planId } = Route.useParams() // Idealmente usa el ID de la ruta const { data } = usePlan(planId) - const [ciclo, setCiclo] = useState(0) + const [totalCiclos, setTotalCiclos] = useState(0) const [editingLineaId, setEditingLineaId] = useState(null) const [tempNombreLinea, setTempNombreLinea] = useState('') const { mutate: createLinea } = useCreateLinea() @@ -365,11 +348,10 @@ function MapaCurricularPage() { const [isEditModalOpen, setIsEditModalOpen] = useState(false) const [nombreNuevaLinea, setNombreNuevaLinea] = useState('') // Para el input de nombre personalizado const { mutate: updateAsignatura } = useUpdateAsignatura() - const [seriacionValue, setSeriacionValue] = useState('') useEffect(() => { if (data?.numero_ciclos) { - setCiclo(data.numero_ciclos) + setTotalCiclos(data.numero_ciclos) } }, [data]) @@ -465,7 +447,7 @@ function MapaCurricularPage() { if (lineasApi) setLineas(mapLineasToLineaCurricular(lineasApi)) }, [lineasApi]) - const ciclosTotales = Number(ciclo) + const ciclosTotales = Number(totalCiclos) const ciclosArray = Array.from({ length: ciclosTotales }, (_, i) => i + 1) const [editingData, setEditingData] = useState(null) const handleIntegerChange = (value: string) => { @@ -538,6 +520,7 @@ function MapaCurricularPage() { const unassignedAsignaturas = asignaturas.filter( (m) => m.ciclo === null || m.lineaCurricularId === null, ) + const unassignedCount = unassignedAsignaturas.length const borrarLinea = (id: string) => { if ( @@ -568,9 +551,9 @@ function MapaCurricularPage() { } // --- Selectores/Cálculos --- - const getTotalesCiclo = (ciclo: number) => { + const getTotalesCiclo = (cicloNumero: number) => { return asignaturas - .filter((m) => m.ciclo === ciclo) + .filter((m) => m.ciclo === cicloNumero) .reduce( (acc, m) => ({ cr: acc.cr + (m.creditos || 0), @@ -601,7 +584,7 @@ function MapaCurricularPage() { const handleDragOver = (e: React.DragEvent) => e.preventDefault() const handleDrop = ( e: React.DragEvent, - ciclo: number | null, + cicloDestino: number | null, lineaId: string | null, ) => { e.preventDefault() @@ -610,12 +593,12 @@ function MapaCurricularPage() { setAsignaturas((prev) => prev.map((m) => m.id === draggedAsignatura - ? { ...m, ciclo, lineaCurricularId: lineaId } + ? { ...m, ciclo: cicloDestino, lineaCurricularId: lineaId } : m, ), ) const patch = { - numero_ciclo: ciclo, + numero_ciclo: cicloDestino, linea_plan_id: lineaId, } @@ -651,7 +634,7 @@ function MapaCurricularPage() { const handleKeyDownLinea = ( e: React.KeyboardEvent, - id: string, + _id: string, ) => { if (e.key === 'Enter') { e.preventDefault() @@ -680,78 +663,77 @@ function MapaCurricularPage() { return
Cargando mapa curricular...
return ( -
+
{/* Header */} -
-
-

Mapa Curricular

-

- Organiza las asignaturas de la petición por línea y ciclo -

-
-
- - {asignaturas.filter((m) => !m.ciclo || !m.lineaCurricularId).length > - 0 && ( - - {' '} - { - asignaturas.filter((m) => !m.ciclo || !m.lineaCurricularId) - .length - }{' '} - sin asignar - - )} - - - + {!tieneAreaComun && ( + - - - {!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" - /> - -
+

+ Crea una línea personalizada sin abrir menús adicionales. +

- - + +
+ setNombreNuevaLinea(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter' && nombreNuevaLinea.trim()) { + manejarAgregarLinea(nombreNuevaLinea) + } + }} + placeholder="Ej: Optativas" + className="h-9" + /> + +
+
+
{/* Barra Totales */} -
+
@@ -760,37 +742,42 @@ function MapaCurricularPage() {
-
+
LÍNEA CURRICULAR
{ciclosArray.map((n) => (
Ciclo {n}
))} -
+
SUBTOTAL
- {lineas.map((linea, idx) => { + {lineas.map((linea) => { const sub = getSubtotalLinea(linea.id) return (
{linea.nombre}
-
+
- borrarLinea(linea.id)} - className="..." - size={14} - /> + className="text-destructive/80 hover:text-destructive inline-flex h-7 w-7 items-center justify-center rounded-md transition-colors" + > + +
- {ciclosArray.map((ciclo) => ( + {ciclosArray.map((cicloNumero) => (
handleDrop(e, ciclo, linea.id)} - className="min-h-35 space-y-2 rounded-xl border-2 border-dashed border-slate-100 bg-slate-50/20" + onDrop={(e) => handleDrop(e, cicloNumero, linea.id)} + className={`min-h-35 space-y-2 rounded-xl border border-dashed p-1.5 transition-colors ${ + draggedAsignatura + ? 'border-primary/35 bg-primary/6' + : 'border-border/70 bg-muted/15' + }`} > {asignaturas .filter( (m) => - m.ciclo === ciclo && m.lineaCurricularId === linea.id, + m.ciclo === cicloNumero && + m.lineaCurricularId === linea.id, ) .map((m) => ( {sub.cr === 0 && sub.hd === 0 && sub.hi === 0 ? ( -
+
) : ( <>
-
+
Cr: {sub.cr}
-
+
HD: {sub.hd} • HI: {sub.hi}
@@ -885,28 +881,30 @@ function MapaCurricularPage() { ) })} -
+
-
+
Totales por Ciclo
- {ciclosArray.map((ciclo) => { - const t = getTotalesCiclo(ciclo) + {ciclosArray.map((cicloNumero) => { + const t = getTotalesCiclo(cicloNumero) const isEmpty = t.cr === 0 && t.hd === 0 && t.hi === 0 return (
{isEmpty ? ( -
+
) : ( <> -
Cr: {t.cr}
+
Cr: {t.cr}
HD: {t.hd} • HI: {t.hi}
@@ -916,7 +914,7 @@ function MapaCurricularPage() { ) })} -
+
{stats.cr} Cr
{stats.hd + stats.hi} Hrs
@@ -960,8 +958,8 @@ function MapaCurricularPage() { onDragOver={handleDragOver} onDrop={(e) => handleDrop(e, null, null)} className={[ - 'rounded-[24px] border-2 border-dashed p-4 transition-all duration-300', - 'min-h-[220px]', + 'rounded-3xl border-2 border-dashed p-4 transition-all duration-300', + 'min-h-55', draggedAsignatura ? 'border-primary/35 bg-primary/6 shadow-inner' : 'border-border bg-muted/20', @@ -970,7 +968,7 @@ function MapaCurricularPage() { {unassignedAsignaturas.length > 0 ? (
{unassignedAsignaturas.map((m) => ( -
+
) : ( -
+
@@ -1007,11 +1005,11 @@ function MapaCurricularPage() { {/* Modal de Edición */} e.preventDefault()} > - + Editar Asignatura @@ -1209,7 +1207,7 @@ function MapaCurricularPage() { ) .map((asig) => ( - + [C{asig.ciclo}] {' '} {asig.nombre} @@ -1249,12 +1247,7 @@ function MapaCurricularPage() { > Cancelar - +
) : ( From 60bfa311861f20c22c67352d3276e06b27228e3e Mon Sep 17 00:00:00 2001 From: Alejandro Rosales Date: Tue, 24 Mar 2026 09:11:40 -0600 Subject: [PATCH 2/2] Refactor MapaCurricularPage: improve line editing functionality and enhance tooltip interactions --- src/routes/planes/$planId/_detalle/mapa.tsx | 122 +++++++++----------- 1 file changed, 57 insertions(+), 65 deletions(-) diff --git a/src/routes/planes/$planId/_detalle/mapa.tsx b/src/routes/planes/$planId/_detalle/mapa.tsx index de321f1..0738284 100644 --- a/src/routes/planes/$planId/_detalle/mapa.tsx +++ b/src/routes/planes/$planId/_detalle/mapa.tsx @@ -1,7 +1,6 @@ -/* eslint-disable jsx-a11y/no-static-element-interactions */ /* eslint-disable jsx-a11y/label-has-associated-control */ import { createFileRoute } from '@tanstack/react-router' -import { Plus, AlertTriangle, Trash2, Pencil, Download } from 'lucide-react' +import { Plus, AlertTriangle, Trash2, Download } from 'lucide-react' import * as Icons from 'lucide-react' import { useMemo, useState, useEffect, Fragment } from 'react' @@ -238,7 +237,7 @@ function AsignaturaCardItem({
- + {estado.label} @@ -249,7 +248,7 @@ function AsignaturaCardItem({ {/* titulo */}

(null) - const [tempNombreLinea, setTempNombreLinea] = useState('') const { mutate: createLinea } = useCreateLinea() const { mutate: updateLineaApi } = useUpdateLinea() const { mutate: deleteLineaApi } = useDeleteLinea() @@ -397,11 +395,8 @@ function MapaCurricularPage() { }, ) } - const guardarEdicionLinea = (id: string, nuevoNombre?: string) => { - // Usamos el nombre que viene por parámetro o el del estado como fallback - const nombreAFijar = ( - nuevoNombre !== undefined ? nuevoNombre : tempNombreLinea - ).trim() + const guardarEdicionLinea = (id: string, nuevoNombre: string) => { + const nombreAFijar = nuevoNombre.trim() if (!nombreAFijar) { setEditingLineaId(null) @@ -421,7 +416,6 @@ function MapaCurricularPage() { ), ) setEditingLineaId(null) - setTempNombreLinea('') }, onError: (err) => { console.error('Error al actualizar linea:', err) @@ -632,38 +626,28 @@ function MapaCurricularPage() { [asignaturas], ) - const handleKeyDownLinea = ( - e: React.KeyboardEvent, - _id: string, - ) => { - if (e.key === 'Enter') { - e.preventDefault() - e.currentTarget.blur() - } - } - - const handleBlurLinea = ( - e: React.FocusEvent, - id: string, - ) => { - const nuevoNombre = e.currentTarget.textContent.trim() || '' - - // Buscamos la línea original para comparar + const confirmarEdicionLinea = (id: string, nuevoNombreRaw: string) => { + const nuevoNombre = nuevoNombreRaw.trim() const lineaOriginal = lineas.find((l) => l.id === id) - if (nuevoNombre !== lineaOriginal?.nombre) { - // IMPORTANTE: Pasamos nuevoNombre directamente - guardarEdicionLinea(id, nuevoNombre) - } else { + if (!nuevoNombre) { setEditingLineaId(null) + return } + + if (nuevoNombre !== lineaOriginal?.nombre) { + guardarEdicionLinea(id, nuevoNombre) + return + } + + setEditingLineaId(null) } if (loadingAsig || loadingLineas) return
Cargando mapa curricular...
return ( -
+
{/* Header */}
@@ -770,7 +754,7 @@ function MapaCurricularPage() { return (
-
- handleKeyDownLinea(e, linea.id)} - onBlur={(e) => handleBlurLinea(e, linea.id)} - onClick={() => { - if (editingLineaId !== linea.id) { - setEditingLineaId(linea.id) - setTempNombreLinea(linea.nombre) - } - }} - className={`text-foreground block w-full truncate text-sm font-bold wrap-break-word outline-none ${ - editingLineaId === linea.id - ? 'border-primary/40 cursor-text border-b pb-1' - : 'cursor-pointer' - }`} - > - {linea.nombre} - +
+ + + setEditingLineaId(linea.id)} + onKeyDown={(e) => { + if (e.key === 'Enter') { + e.preventDefault() + e.currentTarget.blur() + } + if (e.key === 'Escape') { + e.preventDefault() + e.currentTarget.textContent = linea.nombre + e.currentTarget.blur() + } + }} + onBlur={(e) => + confirmarEdicionLinea( + linea.id, + e.currentTarget.textContent, + ) + } + className="text-foreground hover:text-foreground/85 block w-full cursor-text text-sm leading-snug wrap-break-word transition-colors outline-none" + > + {linea.nombre} + + + + {linea.nombre} + +
-
- +