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 was merged in pull request #116.
This commit is contained in:
@@ -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) => {
|
||||||
id: asig.id,
|
return {
|
||||||
clave: asig.codigo,
|
id: asig.id,
|
||||||
nombre: asig.nombre,
|
clave: asig.codigo,
|
||||||
creditos: asig.creditos ?? 0,
|
nombre: asig.nombre,
|
||||||
ciclo: asig.numero_ciclo ?? null,
|
creditos: asig.creditos ?? 0,
|
||||||
lineaCurricularId: asig.linea_plan_id ?? null,
|
ciclo: asig.numero_ciclo ?? null,
|
||||||
tipo: asig.tipo === 'OBLIGATORIA' ? 'obligatoria' : 'optativa',
|
lineaCurricularId: asig.linea_plan_id ?? null,
|
||||||
estado: 'borrador',
|
tipo: asig.tipo,
|
||||||
orden: asig.orden_celda ?? 0,
|
estado: 'borrador',
|
||||||
// Mapeo directo de los nuevos campos de la API
|
orden: asig.orden_celda ?? 0,
|
||||||
hd: asig.horas_academicas ?? 0,
|
// Mapeo directo de los nuevos campos de la API
|
||||||
hi: asig.horas_independientes ?? 0,
|
hd: asig.horas_academicas ?? 0,
|
||||||
prerrequisitos: [],
|
hi: asig.horas_independientes ?? 0,
|
||||||
}))
|
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,138 +588,124 @@ 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 (
|
|
||||||
<div
|
|
||||||
key={linea.id}
|
|
||||||
className="mb-3 grid gap-3"
|
|
||||||
style={{
|
|
||||||
gridTemplateColumns: `220px repeat(${ciclosTotales}, 1fr) 120px`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={`group relative flex items-center justify-between rounded-xl border-l-4 p-4 transition-all ${
|
|
||||||
lineColors[idx % lineColors.length]
|
|
||||||
} ${editingLineaId === linea.id ? 'bg-white ring-2 ring-teal-500/20' : ''}`}
|
|
||||||
>
|
|
||||||
<div className="flex-1 overflow-hidden">
|
|
||||||
<span
|
|
||||||
contentEditable={editingLineaId === linea.id}
|
|
||||||
suppressContentEditableWarning
|
|
||||||
spellCheck={false}
|
|
||||||
onKeyDown={(e) => handleKeyDownLinea(e, linea.id)}
|
|
||||||
onBlur={(e) => handleBlurLinea(e, linea.id)}
|
|
||||||
onClick={() => {
|
|
||||||
if (editingLineaId !== linea.id) {
|
|
||||||
setEditingLineaId(linea.id)
|
|
||||||
setTempNombreLinea(linea.nombre)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className={`block w-full text-xs font-bold break-words outline-none ${
|
|
||||||
editingLineaId === linea.id
|
|
||||||
? 'cursor-text border-b border-teal-500/50 pb-1'
|
|
||||||
: 'cursor-pointer'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{linea.nombre}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
return (
|
||||||
{/* Botón de edición que aparece en hover o si está editando */}
|
<Fragment key={linea.id}>
|
||||||
<button
|
|
||||||
onClick={() => setEditingLineaId(linea.id)}
|
|
||||||
className={`text-slate-400 transition-opacity hover:text-teal-600 ${
|
|
||||||
editingLineaId === linea.id
|
|
||||||
? 'opacity-0'
|
|
||||||
: 'opacity-0 group-hover:opacity-100'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<Pencil size={12} />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<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)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{ciclosArray.map((ciclo) => (
|
|
||||||
<div
|
<div
|
||||||
key={ciclo}
|
className={`group relative flex items-center justify-between rounded-xl border-l-4 p-4 transition-all ${
|
||||||
onDragOver={handleDragOver}
|
lineColors[idx % lineColors.length]
|
||||||
// ANTES: onDrop={(e) => handleDrop(e, null, null)}
|
} ${editingLineaId === linea.id ? 'bg-white ring-2 ring-teal-500/20' : ''}`}
|
||||||
// AHORA: Usamos las variables 'ciclo' y 'linea.id' del map
|
|
||||||
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"
|
|
||||||
>
|
>
|
||||||
{asignaturas
|
<div className="flex-1 overflow-hidden">
|
||||||
.filter(
|
<span
|
||||||
(m) =>
|
contentEditable={editingLineaId === linea.id}
|
||||||
m.ciclo === ciclo && m.lineaCurricularId === linea.id,
|
suppressContentEditableWarning
|
||||||
)
|
spellCheck={false}
|
||||||
.map((m) => (
|
onKeyDown={(e) => handleKeyDownLinea(e, linea.id)}
|
||||||
<AsignaturaCardItem
|
onBlur={(e) => handleBlurLinea(e, linea.id)}
|
||||||
key={m.id}
|
onClick={() => {
|
||||||
asignatura={m}
|
if (editingLineaId !== linea.id) {
|
||||||
isDragging={draggedAsignatura === m.id}
|
setEditingLineaId(linea.id)
|
||||||
onDragStart={handleDragStart}
|
setTempNombreLinea(linea.nombre)
|
||||||
onClick={() => {
|
}
|
||||||
setEditingData(m)
|
}}
|
||||||
setIsEditModalOpen(true)
|
className={`block w-full text-xs font-bold break-words outline-none ${
|
||||||
}}
|
editingLineaId === linea.id
|
||||||
/>
|
? 'cursor-text border-b border-teal-500/50 pb-1'
|
||||||
))}
|
: 'cursor-pointer'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{linea.nombre}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => setEditingLineaId(linea.id)}
|
||||||
|
className="..."
|
||||||
|
>
|
||||||
|
{' '}
|
||||||
|
<Pencil size={12} />{' '}
|
||||||
|
</button>
|
||||||
|
<Trash2
|
||||||
|
onClick={() => borrarLinea(linea.id)}
|
||||||
|
className="..."
|
||||||
|
size={14}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
|
||||||
|
|
||||||
<div className="flex flex-col justify-center rounded-xl border border-slate-100 bg-slate-50 p-4 text-[10px] font-medium text-slate-500">
|
{ciclosArray.map((ciclo) => (
|
||||||
<div>Cr: {sub.cr}</div>
|
<div
|
||||||
<div>HD: {sub.hd}</div>
|
key={`${linea.id}-${ciclo}`}
|
||||||
<div>HI: {sub.hi}</div>
|
onDragOver={handleDragOver}
|
||||||
</div>
|
onDrop={(e) => handleDrop(e, ciclo, linea.id)}
|
||||||
</div>
|
className="min-h-[140px] space-y-2 rounded-xl border-2 border-dashed border-slate-100 bg-slate-50/20 p-2"
|
||||||
)
|
>
|
||||||
})}
|
{asignaturas
|
||||||
|
.filter(
|
||||||
|
(m) =>
|
||||||
|
m.ciclo === ciclo &&
|
||||||
|
m.lineaCurricularId === linea.id,
|
||||||
|
)
|
||||||
|
.map((m) => (
|
||||||
|
<AsignaturaCardItem
|
||||||
|
key={m.id}
|
||||||
|
asignatura={m}
|
||||||
|
isDragging={draggedAsignatura === m.id}
|
||||||
|
onDragStart={handleDragStart}
|
||||||
|
onClick={() => {
|
||||||
|
setEditingData(m)
|
||||||
|
setIsEditModalOpen(true)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
<div
|
<div className="flex flex-col justify-center rounded-xl border border-slate-100 bg-slate-50 p-4 text-[10px] font-medium text-slate-500">
|
||||||
className="mt-6 grid gap-3 border-t pt-4"
|
<div>Cr: {sub.cr}</div>
|
||||||
style={{
|
<div>HD: {sub.hd}</div>
|
||||||
gridTemplateColumns: `220px repeat(${ciclosTotales}, 1fr) 120px`,
|
<div>HI: {sub.hi}</div>
|
||||||
}}
|
</div>
|
||||||
>
|
</Fragment>
|
||||||
<div className="p-2 font-bold text-slate-600">
|
)
|
||||||
|
})}
|
||||||
|
|
||||||
|
<div className="col-span-full my-2 border-t border-slate-200"></div>
|
||||||
|
|
||||||
|
<div className="self-center 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>
|
||||||
|
|||||||
Reference in New Issue
Block a user