se arregló el estilo visual y comportamiento del grid del mapa curricular

fix #108: ahora se utiliza un único grid para todo el mapa curricular. de esta manera el espaciado se mantiene consistente
This commit is contained in:
2026-02-13 14:12:32 -06:00
parent 13d9f1fe4a
commit da218b1f92

View File

@@ -9,9 +9,11 @@ import {
Trash2, Trash2,
Pencil, Pencil,
} from 'lucide-react' } from 'lucide-react'
import { useMemo, useState, useEffect } from 'react' import { useMemo, useState, useEffect, Fragment } from 'react'
import type { TipoAsignatura } from '@/data'
import type { Asignatura, LineaCurricular } from '@/types/plan' import type { Asignatura, LineaCurricular } from '@/types/plan'
import type { TablesUpdate } from '@/types/supabase'
import { Badge } from '@/components/ui/badge' import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
@@ -60,21 +62,23 @@ const mapLineasToLineaCurricular = (
const mapAsignaturasToAsignaturas = ( const mapAsignaturasToAsignaturas = (
asigApi: Array<any> = [], asigApi: Array<any> = [],
): Array<Asignatura> => { ): Array<Asignatura> => {
return asigApi.map((asig) => ({ return asigApi.map((asig) => {
return {
id: asig.id, id: asig.id,
clave: asig.codigo, clave: asig.codigo,
nombre: asig.nombre, nombre: asig.nombre,
creditos: asig.creditos ?? 0, creditos: asig.creditos ?? 0,
ciclo: asig.numero_ciclo ?? null, ciclo: asig.numero_ciclo ?? null,
lineaCurricularId: asig.linea_plan_id ?? null, lineaCurricularId: asig.linea_plan_id ?? null,
tipo: asig.tipo === 'OBLIGATORIA' ? 'obligatoria' : 'optativa', tipo: asig.tipo,
estado: 'borrador', estado: 'borrador',
orden: asig.orden_celda ?? 0, orden: asig.orden_celda ?? 0,
// Mapeo directo de los nuevos campos de la API // Mapeo directo de los nuevos campos de la API
hd: asig.horas_academicas ?? 0, hd: asig.horas_academicas ?? 0,
hi: asig.horas_independientes ?? 0, hi: asig.horas_independientes ?? 0,
prerrequisitos: [], prerrequisitos: [],
})) }
})
} }
const lineColors = [ const lineColors = [
@@ -190,7 +194,7 @@ function MapaCurricularPage() {
const [isEditModalOpen, setIsEditModalOpen] = useState(false) const [isEditModalOpen, setIsEditModalOpen] = useState(false)
const [nombreNuevaLinea, setNombreNuevaLinea] = useState('') // Para el input de nombre personalizado const [nombreNuevaLinea, setNombreNuevaLinea] = useState('') // Para el input de nombre personalizado
const { mutate: updateAsignatura } = useUpdateAsignatura() const { mutate: updateAsignatura } = useUpdateAsignatura()
const [seriacionValue, setSeriacionValue] = useState<string>('unassigned') const [seriacionValue, setSeriacionValue] = useState<string>('')
useEffect(() => { useEffect(() => {
if (data?.numero_ciclos) { if (data?.numero_ciclos) {
@@ -323,7 +327,17 @@ function MapaCurricularPage() {
setAsignaturas((prev) => setAsignaturas((prev) =>
prev.map((m) => (m.id === editingData.id ? { ...editingData } : m)), prev.map((m) => (m.id === editingData.id ? { ...editingData } : m)),
) )
const patch = { type AsignaturaPatch = {
codigo?: TablesUpdate<'asignaturas'>['codigo']
nombre?: TablesUpdate<'asignaturas'>['nombre']
tipo?: TablesUpdate<'asignaturas'>['tipo']
creditos?: TablesUpdate<'asignaturas'>['creditos']
horas_academicas?: TablesUpdate<'asignaturas'>['horas_academicas']
horas_independientes?: TablesUpdate<'asignaturas'>['horas_independientes']
numero_ciclo?: TablesUpdate<'asignaturas'>['numero_ciclo']
linea_plan_id?: TablesUpdate<'asignaturas'>['linea_plan_id']
}
const patch: Partial<AsignaturaPatch> = {
nombre: editingData.nombre, nombre: editingData.nombre,
codigo: editingData.clave, codigo: editingData.clave,
creditos: editingData.creditos, creditos: editingData.creditos,
@@ -331,12 +345,11 @@ function MapaCurricularPage() {
horas_independientes: editingData.hi, horas_independientes: editingData.hi,
numero_ciclo: editingData.ciclo, numero_ciclo: editingData.ciclo,
linea_plan_id: editingData.lineaCurricularId, linea_plan_id: editingData.lineaCurricularId,
tipo: editingData.tipo.toUpperCase(), // Asegurar que coincida con el ENUM (OBLIGATORIA/OPTATIVA) tipo: editingData.tipo.toUpperCase() as TipoAsignatura, // Asegurar que coincida con el ENUM (OBLIGATORIA/OPTATIVA)
// datos: editingData.datos, // Si editaste algo del JSONB
} }
updateAsignatura( updateAsignatura(
{ asignaturaId: editingData.id, patch }, { asignaturaId: editingData.id, patch: patch as any },
{ {
onSuccess: () => { onSuccess: () => {
setIsEditModalOpen(false) setIsEditModalOpen(false)
@@ -575,37 +588,33 @@ function MapaCurricularPage() {
<div className="overflow-x-auto pb-6"> <div className="overflow-x-auto pb-6">
<div className="min-w-[1500px]"> <div className="min-w-[1500px]">
<div <div
className="mb-4 grid gap-3" className="grid gap-3"
style={{ style={{
gridTemplateColumns: `220px repeat(${ciclosTotales}, 1fr) 120px`, gridTemplateColumns: `220px repeat(${ciclosTotales}, minmax(auto, 1fr)) 120px`,
}} }}
> >
<div className="self-end px-2 text-xs font-bold text-slate-400"> <div className="self-end px-2 text-xs font-bold text-slate-400">
LÍNEA CURRICULAR LÍNEA CURRICULAR
</div> </div>
{ciclosArray.map((n) => ( {ciclosArray.map((n) => (
<div <div
key={n} key={`header-${n}`}
className="rounded-lg bg-slate-100 p-2 text-center text-sm font-bold text-slate-600" className="rounded-lg bg-slate-100 p-2 text-center text-sm font-bold text-slate-600"
> >
Ciclo {n} Ciclo {n}
</div> </div>
))} ))}
<div className="self-end text-center text-xs font-bold text-slate-400"> <div className="self-end text-center text-xs font-bold text-slate-400">
SUBTOTAL SUBTOTAL
</div> </div>
</div>
{lineas.map((linea, idx) => { {lineas.map((linea, idx) => {
const sub = getSubtotalLinea(linea.id) const sub = getSubtotalLinea(linea.id)
return ( return (
<div <Fragment key={linea.id}>
key={linea.id}
className="mb-3 grid gap-3"
style={{
gridTemplateColumns: `220px repeat(${ciclosTotales}, 1fr) 120px`,
}}
>
<div <div
className={`group relative flex items-center justify-between rounded-xl border-l-4 p-4 transition-all ${ className={`group relative flex items-center justify-between rounded-xl border-l-4 p-4 transition-all ${
lineColors[idx % lineColors.length] lineColors[idx % lineColors.length]
@@ -633,41 +642,34 @@ function MapaCurricularPage() {
{linea.nombre} {linea.nombre}
</span> </span>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{/* Botón de edición que aparece en hover o si está editando */}
<button <button
onClick={() => setEditingLineaId(linea.id)} onClick={() => setEditingLineaId(linea.id)}
className={`text-slate-400 transition-opacity hover:text-teal-600 ${ className="..."
editingLineaId === linea.id
? 'opacity-0'
: 'opacity-0 group-hover:opacity-100'
}`}
> >
<Pencil size={12} /> {' '}
<Pencil size={12} />{' '}
</button> </button>
<Trash2 <Trash2
size={14}
className="cursor-pointer text-slate-400 opacity-0 transition-opacity group-hover:opacity-100 hover:text-red-500"
onClick={() => borrarLinea(linea.id)} onClick={() => borrarLinea(linea.id)}
className="..."
size={14}
/> />
</div> </div>
</div> </div>
{ciclosArray.map((ciclo) => ( {ciclosArray.map((ciclo) => (
<div <div
key={ciclo} key={`${linea.id}-${ciclo}`}
onDragOver={handleDragOver} onDragOver={handleDragOver}
// ANTES: onDrop={(e) => handleDrop(e, null, null)}
// AHORA: Usamos las variables 'ciclo' y 'linea.id' del map
onDrop={(e) => handleDrop(e, ciclo, linea.id)} onDrop={(e) => handleDrop(e, ciclo, linea.id)}
className="min-h-[140px] space-y-2 rounded-xl border-2 border-dashed border-slate-100 bg-slate-50/20 p-2" className="min-h-[140px] space-y-2 rounded-xl border-2 border-dashed border-slate-100 bg-slate-50/20 p-2"
> >
{asignaturas {asignaturas
.filter( .filter(
(m) => (m) =>
m.ciclo === ciclo && m.lineaCurricularId === linea.id, m.ciclo === ciclo &&
m.lineaCurricularId === linea.id,
) )
.map((m) => ( .map((m) => (
<AsignaturaCardItem <AsignaturaCardItem
@@ -689,24 +691,21 @@ function MapaCurricularPage() {
<div>HD: {sub.hd}</div> <div>HD: {sub.hd}</div>
<div>HI: {sub.hi}</div> <div>HI: {sub.hi}</div>
</div> </div>
</div> </Fragment>
) )
})} })}
<div <div className="col-span-full my-2 border-t border-slate-200"></div>
className="mt-6 grid gap-3 border-t pt-4"
style={{ <div className="self-center p-2 font-bold text-slate-600">
gridTemplateColumns: `220px repeat(${ciclosTotales}, 1fr) 120px`,
}}
>
<div className="p-2 font-bold text-slate-600">
Totales por Ciclo Totales por Ciclo
</div> </div>
{ciclosArray.map((ciclo) => { {ciclosArray.map((ciclo) => {
const t = getTotalesCiclo(ciclo) const t = getTotalesCiclo(ciclo)
return ( return (
<div <div
key={ciclo} key={`footer-${ciclo}`}
className="rounded-lg bg-slate-50 p-2 text-center text-[10px]" className="rounded-lg bg-slate-50 p-2 text-center text-[10px]"
> >
<div className="font-bold text-slate-700">Cr: {t.cr}</div> <div className="font-bold text-slate-700">Cr: {t.cr}</div>
@@ -716,6 +715,7 @@ function MapaCurricularPage() {
</div> </div>
) )
})} })}
<div className="flex flex-col justify-center rounded-lg bg-teal-50 p-2 text-center text-xs font-bold text-teal-800"> <div className="flex flex-col justify-center rounded-lg bg-teal-50 p-2 text-center text-xs font-bold text-teal-800">
<div>{stats.cr} Cr</div> <div>{stats.cr} Cr</div>
<div>{stats.hd + stats.hi} Hrs</div> <div>{stats.hd + stats.hi} Hrs</div>
@@ -725,7 +725,6 @@ function MapaCurricularPage() {
</div> </div>
{/* Asignaturas Sin Asignar */} {/* Asignaturas Sin Asignar */}
{/* SECCIÓN DE MATERIAS SIN ASIGNAR (Mejorada para estar siempre disponible) */}
<div className="mt-10 rounded-2xl border border-slate-200 bg-slate-50 p-6"> <div className="mt-10 rounded-2xl border border-slate-200 bg-slate-50 p-6">
<div className="mb-4 flex items-center justify-between"> <div className="mb-4 flex items-center justify-between">
<div className="flex items-center gap-2 text-slate-600"> <div className="flex items-center gap-2 text-slate-600">
@@ -941,8 +940,8 @@ function MapaCurricularPage() {
<Select <Select
value={seriacionValue} value={seriacionValue}
onValueChange={(val) => { onValueChange={(val) => {
if (val === 'unassigned') { if (val === 'none') {
setSeriacionValue('unassigned') setSeriacionValue('')
return return
} }
if (!editingData.prerrequisitos.includes(val)) { if (!editingData.prerrequisitos.includes(val)) {
@@ -951,21 +950,19 @@ function MapaCurricularPage() {
prerrequisitos: [...editingData.prerrequisitos, val], prerrequisitos: [...editingData.prerrequisitos, val],
}) })
} }
setSeriacionValue('unassigned') setSeriacionValue('')
}} }}
> >
<SelectTrigger> <SelectTrigger>
<SelectValue placeholder="Seleccionar asignatura..." /> <SelectValue placeholder="Seleccionar asignatura..." />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="unassigned"> <SelectItem value="none">-- Sin Seriación --</SelectItem>
-- Sin Seriación --
</SelectItem>
{asignaturas {asignaturas
.filter((m) => m.id !== editingData.id) .filter((m) => m.id !== editingData.id)
.map((m) => ( .map((m) => (
<SelectItem key={m.id} value={m.clave}> <SelectItem key={m.id} value={m.id}>
{m.nombre} ({m.clave}) {m.nombre} ({m.clave})
</SelectItem> </SelectItem>
))} ))}
@@ -1006,7 +1003,7 @@ function MapaCurricularPage() {
</label> </label>
<Select <Select
value={editingData.tipo} value={editingData.tipo}
onValueChange={(val: 'obligatoria' | 'optativa') => onValueChange={(val: 'OBLIGATORIA' | 'OPTATIVA') =>
setEditingData({ ...editingData, tipo: val }) setEditingData({ ...editingData, tipo: val })
} }
> >
@@ -1014,8 +1011,8 @@ function MapaCurricularPage() {
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="obligatoria">Obligatoria</SelectItem> <SelectItem value="OBLIGATORIA">Obligatoria</SelectItem>
<SelectItem value="optativa">Optativa</SelectItem> <SelectItem value="OPTATIVA">Optativa</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>