Merge pull request 'Refactor MapaCurricularPage: update state variables and enhance UI components for better clarity and usability' (#220) from features/mejora-de-estilos into main
Deploy to Azure Static Web Apps / build-and-deploy (push) Successful in 1m34s
Deploy to Azure Static Web Apps / build-and-deploy (push) Successful in 1m34s
Reviewed-on: #220
This commit was merged in pull request #220.
This commit is contained in:
@@ -73,7 +73,7 @@ const mapAsignaturas = (
|
|||||||
estado: asig.estado,
|
estado: asig.estado,
|
||||||
hd: asig.horas_academicas ?? 0,
|
hd: asig.horas_academicas ?? 0,
|
||||||
hi: asig.horas_independientes ?? 0,
|
hi: asig.horas_independientes ?? 0,
|
||||||
prerrequisitos: [],
|
prerrequisito_asignatura_id: asig.prerrequisito_asignatura_id ?? null,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
/* eslint-disable jsx-a11y/no-static-element-interactions */
|
|
||||||
/* eslint-disable jsx-a11y/label-has-associated-control */
|
/* eslint-disable jsx-a11y/label-has-associated-control */
|
||||||
import { createFileRoute } from '@tanstack/react-router'
|
import { createFileRoute } from '@tanstack/react-router'
|
||||||
import { Plus, ChevronDown, AlertTriangle, Trash2, Pencil } from 'lucide-react'
|
import { Plus, AlertTriangle, Trash2, Download } from 'lucide-react'
|
||||||
import * as Icons from 'lucide-react'
|
import * as Icons from 'lucide-react'
|
||||||
import { useMemo, useState, useEffect, Fragment } from 'react'
|
import { useMemo, useState, useEffect, Fragment } from 'react'
|
||||||
|
|
||||||
@@ -17,12 +16,6 @@ import {
|
|||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from '@/components/ui/dialog'
|
} from '@/components/ui/dialog'
|
||||||
import {
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
} from '@/components/ui/dropdown-menu'
|
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
@@ -92,19 +85,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<string, string> = {
|
|
||||||
borrador: 'bg-slate-100 text-slate-600',
|
|
||||||
revisada: 'bg-amber-100 text-amber-700',
|
|
||||||
aprobada: 'bg-emerald-100 text-emerald-700',
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Subcomponentes ---
|
// --- Subcomponentes ---
|
||||||
function StatItem({
|
function StatItem({
|
||||||
label,
|
label,
|
||||||
@@ -116,14 +96,16 @@ function StatItem({
|
|||||||
total?: number
|
total?: number
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-baseline gap-1.5">
|
<div className="border-border/70 bg-background/75 rounded-xl border p-3">
|
||||||
<span className="text-[10px] font-bold tracking-wider text-slate-400 uppercase">
|
<span className="text-muted-foreground block text-[11px] font-semibold tracking-wide uppercase">
|
||||||
{label}:
|
{label}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-sm font-bold text-slate-700">
|
<span className="text-foreground mt-1 block text-lg leading-none font-bold">
|
||||||
{value}
|
{value}
|
||||||
{total ? (
|
{total ? (
|
||||||
<span className="font-normal text-slate-400">/{total}</span>
|
<span className="text-muted-foreground ml-1 text-sm font-medium">
|
||||||
|
/{total}
|
||||||
|
</span>
|
||||||
) : (
|
) : (
|
||||||
''
|
''
|
||||||
)}
|
)}
|
||||||
@@ -255,7 +237,7 @@ function AsignaturaCardItem({
|
|||||||
<EstadoIcon className="text-foreground/65 h-3.5 w-3.5" />
|
<EstadoIcon className="text-foreground/65 h-3.5 w-3.5" />
|
||||||
</div>
|
</div>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent side="left">
|
<TooltipContent side="right">
|
||||||
<span className="text-xs font-semibold">
|
<span className="text-xs font-semibold">
|
||||||
{estado.label}
|
{estado.label}
|
||||||
</span>
|
</span>
|
||||||
@@ -266,7 +248,7 @@ function AsignaturaCardItem({
|
|||||||
{/* titulo */}
|
{/* titulo */}
|
||||||
<div className="mt-4 min-h-18">
|
<div className="mt-4 min-h-18">
|
||||||
<h3
|
<h3
|
||||||
className="text-foreground text-md overflow-hidden leading-[1.08] font-bold"
|
className="text-foreground text-md overflow-hidden leading-[1.08]"
|
||||||
style={{
|
style={{
|
||||||
display: '-webkit-box',
|
display: '-webkit-box',
|
||||||
WebkitLineClamp: 3,
|
WebkitLineClamp: 3,
|
||||||
@@ -348,9 +330,8 @@ export const Route = createFileRoute('/planes/$planId/_detalle/mapa')({
|
|||||||
function MapaCurricularPage() {
|
function MapaCurricularPage() {
|
||||||
const { planId } = Route.useParams() // Idealmente usa el ID de la ruta
|
const { planId } = Route.useParams() // Idealmente usa el ID de la ruta
|
||||||
const { data } = usePlan(planId)
|
const { data } = usePlan(planId)
|
||||||
const [ciclo, setCiclo] = useState(0)
|
const [totalCiclos, setTotalCiclos] = useState(0)
|
||||||
const [editingLineaId, setEditingLineaId] = useState<string | null>(null)
|
const [editingLineaId, setEditingLineaId] = useState<string | null>(null)
|
||||||
const [tempNombreLinea, setTempNombreLinea] = useState('')
|
|
||||||
const { mutate: createLinea } = useCreateLinea()
|
const { mutate: createLinea } = useCreateLinea()
|
||||||
const { mutate: updateLineaApi } = useUpdateLinea()
|
const { mutate: updateLineaApi } = useUpdateLinea()
|
||||||
const { mutate: deleteLineaApi } = useDeleteLinea()
|
const { mutate: deleteLineaApi } = useDeleteLinea()
|
||||||
@@ -365,11 +346,10 @@ 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>('')
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data?.numero_ciclos) {
|
if (data?.numero_ciclos) {
|
||||||
setCiclo(data.numero_ciclos)
|
setTotalCiclos(data.numero_ciclos)
|
||||||
}
|
}
|
||||||
}, [data])
|
}, [data])
|
||||||
|
|
||||||
@@ -415,11 +395,8 @@ function MapaCurricularPage() {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
const guardarEdicionLinea = (id: string, nuevoNombre?: string) => {
|
const guardarEdicionLinea = (id: string, nuevoNombre: string) => {
|
||||||
// Usamos el nombre que viene por parámetro o el del estado como fallback
|
const nombreAFijar = nuevoNombre.trim()
|
||||||
const nombreAFijar = (
|
|
||||||
nuevoNombre !== undefined ? nuevoNombre : tempNombreLinea
|
|
||||||
).trim()
|
|
||||||
|
|
||||||
if (!nombreAFijar) {
|
if (!nombreAFijar) {
|
||||||
setEditingLineaId(null)
|
setEditingLineaId(null)
|
||||||
@@ -439,7 +416,6 @@ function MapaCurricularPage() {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
setEditingLineaId(null)
|
setEditingLineaId(null)
|
||||||
setTempNombreLinea('')
|
|
||||||
},
|
},
|
||||||
onError: (err) => {
|
onError: (err) => {
|
||||||
console.error('Error al actualizar linea:', err)
|
console.error('Error al actualizar linea:', err)
|
||||||
@@ -465,7 +441,7 @@ function MapaCurricularPage() {
|
|||||||
if (lineasApi) setLineas(mapLineasToLineaCurricular(lineasApi))
|
if (lineasApi) setLineas(mapLineasToLineaCurricular(lineasApi))
|
||||||
}, [lineasApi])
|
}, [lineasApi])
|
||||||
|
|
||||||
const ciclosTotales = Number(ciclo)
|
const ciclosTotales = Number(totalCiclos)
|
||||||
const ciclosArray = Array.from({ length: ciclosTotales }, (_, i) => i + 1)
|
const ciclosArray = Array.from({ length: ciclosTotales }, (_, i) => i + 1)
|
||||||
const [editingData, setEditingData] = useState<Asignatura | null>(null)
|
const [editingData, setEditingData] = useState<Asignatura | null>(null)
|
||||||
const handleIntegerChange = (value: string) => {
|
const handleIntegerChange = (value: string) => {
|
||||||
@@ -538,6 +514,7 @@ function MapaCurricularPage() {
|
|||||||
const unassignedAsignaturas = asignaturas.filter(
|
const unassignedAsignaturas = asignaturas.filter(
|
||||||
(m) => m.ciclo === null || m.lineaCurricularId === null,
|
(m) => m.ciclo === null || m.lineaCurricularId === null,
|
||||||
)
|
)
|
||||||
|
const unassignedCount = unassignedAsignaturas.length
|
||||||
|
|
||||||
const borrarLinea = (id: string) => {
|
const borrarLinea = (id: string) => {
|
||||||
if (
|
if (
|
||||||
@@ -568,9 +545,9 @@ function MapaCurricularPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --- Selectores/Cálculos ---
|
// --- Selectores/Cálculos ---
|
||||||
const getTotalesCiclo = (ciclo: number) => {
|
const getTotalesCiclo = (cicloNumero: number) => {
|
||||||
return asignaturas
|
return asignaturas
|
||||||
.filter((m) => m.ciclo === ciclo)
|
.filter((m) => m.ciclo === cicloNumero)
|
||||||
.reduce(
|
.reduce(
|
||||||
(acc, m) => ({
|
(acc, m) => ({
|
||||||
cr: acc.cr + (m.creditos || 0),
|
cr: acc.cr + (m.creditos || 0),
|
||||||
@@ -601,7 +578,7 @@ function MapaCurricularPage() {
|
|||||||
const handleDragOver = (e: React.DragEvent) => e.preventDefault()
|
const handleDragOver = (e: React.DragEvent) => e.preventDefault()
|
||||||
const handleDrop = (
|
const handleDrop = (
|
||||||
e: React.DragEvent,
|
e: React.DragEvent,
|
||||||
ciclo: number | null,
|
cicloDestino: number | null,
|
||||||
lineaId: string | null,
|
lineaId: string | null,
|
||||||
) => {
|
) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
@@ -610,12 +587,12 @@ function MapaCurricularPage() {
|
|||||||
setAsignaturas((prev) =>
|
setAsignaturas((prev) =>
|
||||||
prev.map((m) =>
|
prev.map((m) =>
|
||||||
m.id === draggedAsignatura
|
m.id === draggedAsignatura
|
||||||
? { ...m, ciclo, lineaCurricularId: lineaId }
|
? { ...m, ciclo: cicloDestino, lineaCurricularId: lineaId }
|
||||||
: m,
|
: m,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
const patch = {
|
const patch = {
|
||||||
numero_ciclo: ciclo,
|
numero_ciclo: cicloDestino,
|
||||||
linea_plan_id: lineaId,
|
linea_plan_id: lineaId,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -649,109 +626,98 @@ function MapaCurricularPage() {
|
|||||||
[asignaturas],
|
[asignaturas],
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleKeyDownLinea = (
|
const confirmarEdicionLinea = (id: string, nuevoNombreRaw: string) => {
|
||||||
e: React.KeyboardEvent<HTMLSpanElement>,
|
const nuevoNombre = nuevoNombreRaw.trim()
|
||||||
id: string,
|
|
||||||
) => {
|
|
||||||
if (e.key === 'Enter') {
|
|
||||||
e.preventDefault()
|
|
||||||
e.currentTarget.blur()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleBlurLinea = (
|
|
||||||
e: React.FocusEvent<HTMLSpanElement>,
|
|
||||||
id: string,
|
|
||||||
) => {
|
|
||||||
const nuevoNombre = e.currentTarget.textContent.trim() || ''
|
|
||||||
|
|
||||||
// Buscamos la línea original para comparar
|
|
||||||
const lineaOriginal = lineas.find((l) => l.id === id)
|
const lineaOriginal = lineas.find((l) => l.id === id)
|
||||||
|
|
||||||
if (nuevoNombre !== lineaOriginal?.nombre) {
|
if (!nuevoNombre) {
|
||||||
// IMPORTANTE: Pasamos nuevoNombre directamente
|
|
||||||
guardarEdicionLinea(id, nuevoNombre)
|
|
||||||
} else {
|
|
||||||
setEditingLineaId(null)
|
setEditingLineaId(null)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (nuevoNombre !== lineaOriginal?.nombre) {
|
||||||
|
guardarEdicionLinea(id, nuevoNombre)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setEditingLineaId(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loadingAsig || loadingLineas)
|
if (loadingAsig || loadingLineas)
|
||||||
return <div className="p-10 text-center">Cargando mapa curricular...</div>
|
return <div className="p-10 text-center">Cargando mapa curricular...</div>
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container">
|
<div className="space-y-6">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="mb-6 flex items-center justify-between">
|
<div className="border-border bg-card/70 rounded-2xl border p-4 shadow-sm">
|
||||||
<div>
|
<div className="grid gap-4 lg:grid-cols-[minmax(0,1fr)_auto] lg:items-start">
|
||||||
<h2 className="text-xl font-bold">Mapa Curricular</h2>
|
<div className="space-y-1.5">
|
||||||
<p className="text-sm text-slate-500">
|
<h2 className="text-2xl font-bold">Mapa Curricular</h2>
|
||||||
Organiza las asignaturas de la petición por línea y ciclo
|
<p className="text-muted-foreground text-sm">
|
||||||
</p>
|
Organiza las asignaturas de la petición por línea y ciclo
|
||||||
</div>
|
</p>
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<Button className="bg-teal-700 text-white hover:bg-teal-800">
|
{unassignedCount > 0 && (
|
||||||
<Plus size={16} className="mr-2" /> Exportar{' '}
|
<Badge className="border-border bg-accent/50 text-accent-foreground hover:bg-accent/50 mt-2 inline-flex">
|
||||||
</Button>
|
<AlertTriangle size={14} className="mr-1" />
|
||||||
{asignaturas.filter((m) => !m.ciclo || !m.lineaCurricularId).length >
|
{unassignedCount} sin asignar
|
||||||
0 && (
|
</Badge>
|
||||||
<Badge className="border-amber-100 bg-amber-50 text-amber-600 hover:bg-amber-50">
|
)}
|
||||||
<AlertTriangle size={14} className="mr-1" />{' '}
|
</div>
|
||||||
{
|
|
||||||
asignaturas.filter((m) => !m.ciclo || !m.lineaCurricularId)
|
<div className="flex items-center justify-start gap-2 lg:justify-end">
|
||||||
.length
|
<Button variant="outline" className="gap-2">
|
||||||
}{' '}
|
<Download size={16} /> Exportar
|
||||||
sin asignar
|
</Button>
|
||||||
</Badge>
|
{!tieneAreaComun && (
|
||||||
)}
|
<Button
|
||||||
<DropdownMenu>
|
variant="outline"
|
||||||
<DropdownMenuTrigger asChild>
|
className="text-primary border-primary/30 hover:bg-primary/8"
|
||||||
<Button className="bg-teal-700 text-white hover:bg-teal-800">
|
onClick={() => manejarAgregarLinea('Área Común')}
|
||||||
<Plus size={16} className="mr-2" /> Agregar{' '}
|
>
|
||||||
<ChevronDown size={14} className="ml-2" />
|
+ Área Común
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
)}
|
||||||
<DropdownMenuContent align="end">
|
</div>
|
||||||
{!tieneAreaComun && (
|
|
||||||
<>
|
<div className="border-border/70 bg-background/70 rounded-xl border p-3 lg:col-span-2">
|
||||||
<DropdownMenuItem
|
<div className="flex flex-col gap-2 sm:flex-row sm:items-end sm:justify-between">
|
||||||
onClick={() => manejarAgregarLinea('Área Común')}
|
<div className="space-y-1">
|
||||||
className="font-bold text-teal-700"
|
<label className="text-muted-foreground text-[11px] font-semibold tracking-wide uppercase">
|
||||||
>
|
Nueva Línea Curricular
|
||||||
+ Agregar Área Común
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<div className="my-1 border-t border-slate-100" />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{/* Input para nombre personalizado */}
|
|
||||||
<div className="p-2">
|
|
||||||
<label className="text-[10px] font-bold text-slate-400 uppercase">
|
|
||||||
Nombre de Línea
|
|
||||||
</label>
|
</label>
|
||||||
<div className="mt-1 flex gap-1">
|
<p className="text-muted-foreground text-xs">
|
||||||
<Input
|
Crea una línea personalizada sin abrir menús adicionales.
|
||||||
value={nombreNuevaLinea}
|
</p>
|
||||||
onChange={(e) => setNombreNuevaLinea(e.target.value)}
|
|
||||||
placeholder="Ej: Optativas"
|
|
||||||
className="h-8 text-xs"
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
className="h-8 px-2"
|
|
||||||
onClick={() => manejarAgregarLinea(nombreNuevaLinea)}
|
|
||||||
disabled={!nombreNuevaLinea.trim()}
|
|
||||||
>
|
|
||||||
<Plus size={14} />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
<div className="flex w-full gap-2 sm:w-auto sm:min-w-90">
|
||||||
|
<Input
|
||||||
|
value={nombreNuevaLinea}
|
||||||
|
onChange={(e) => setNombreNuevaLinea(e.target.value)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter' && nombreNuevaLinea.trim()) {
|
||||||
|
manejarAgregarLinea(nombreNuevaLinea)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
placeholder="Ej: Optativas"
|
||||||
|
className="h-9"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
className="h-9 gap-1.5"
|
||||||
|
onClick={() => manejarAgregarLinea(nombreNuevaLinea)}
|
||||||
|
disabled={!nombreNuevaLinea.trim()}
|
||||||
|
>
|
||||||
|
<Plus size={14} /> Agregar
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Barra Totales */}
|
{/* Barra Totales */}
|
||||||
<div className="mb-8 flex gap-10 rounded-xl border border-slate-200 bg-slate-50/80 p-4">
|
<div className="border-border bg-card/60 mb-8 grid grid-cols-2 gap-3 rounded-2xl border p-3 shadow-sm md:grid-cols-4">
|
||||||
<StatItem label="Total Créditos" value={stats.cr} total={320} />
|
<StatItem label="Total Créditos" value={stats.cr} total={320} />
|
||||||
<StatItem label="Total HD" value={stats.hd} />
|
<StatItem label="Total HD" value={stats.hd} />
|
||||||
<StatItem label="Total HI" value={stats.hi} />
|
<StatItem label="Total HI" value={stats.hi} />
|
||||||
@@ -760,87 +726,109 @@ function MapaCurricularPage() {
|
|||||||
|
|
||||||
<div className="overflow-x-auto pb-6">
|
<div className="overflow-x-auto pb-6">
|
||||||
<div
|
<div
|
||||||
className="grid gap-2"
|
className="grid gap-3"
|
||||||
style={{
|
style={{
|
||||||
gridTemplateColumns: `140px repeat(${ciclosTotales}, minmax(auto, 1fr)) 120px`,
|
gridTemplateColumns: `140px repeat(${ciclosTotales}, minmax(auto, 1fr)) 120px`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="self-end px-2 text-xs font-bold text-slate-400">
|
<div className="text-muted-foreground self-end px-2 text-xs font-bold">
|
||||||
LÍNEA CURRICULAR
|
LÍNEA CURRICULAR
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{ciclosArray.map((n) => (
|
{ciclosArray.map((n) => (
|
||||||
<div
|
<div
|
||||||
key={`header-${n}`}
|
key={`header-${n}`}
|
||||||
className="rounded-lg bg-slate-100 p-2 text-center text-sm font-bold text-slate-600"
|
className="bg-muted/70 text-muted-foreground border-border/70 rounded-xl border p-2 text-center text-sm font-bold"
|
||||||
>
|
>
|
||||||
Ciclo {n}
|
Ciclo {n}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
<div className="self-end text-center text-xs font-bold text-slate-400">
|
<div className="text-muted-foreground self-end text-center text-xs font-bold">
|
||||||
SUBTOTAL
|
SUBTOTAL
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{lineas.map((linea, idx) => {
|
{lineas.map((linea) => {
|
||||||
const sub = getSubtotalLinea(linea.id)
|
const sub = getSubtotalLinea(linea.id)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment key={linea.id}>
|
<Fragment key={linea.id}>
|
||||||
<div
|
<div
|
||||||
className={`group relative flex items-center justify-between rounded-xl border-l-4 p-3 transition-all ${
|
className={`group relative flex items-start justify-between gap-2 rounded-xl border p-3 transition-all ${editingLineaId === linea.id ? 'ring-primary/30 ring-2' : 'cursor-text'}`}
|
||||||
lineColors[idx % lineColors.length]
|
style={{
|
||||||
} ${editingLineaId === linea.id ? 'bg-white ring-2 ring-teal-500/20' : ''}`}
|
borderColor: hexToRgba(linea.color || '#1976d2', 0.24),
|
||||||
|
backgroundColor:
|
||||||
|
editingLineaId === linea.id
|
||||||
|
? hexToRgba(linea.color || '#1976d2', 0.12)
|
||||||
|
: hexToRgba(linea.color || '#1976d2', 0.08),
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div className="min-w-0 flex-1 overflow-hidden">
|
<div className="min-w-0 flex-1">
|
||||||
<span
|
<Tooltip>
|
||||||
contentEditable={editingLineaId === linea.id}
|
<TooltipTrigger asChild>
|
||||||
suppressContentEditableWarning
|
<span
|
||||||
spellCheck={false}
|
contentEditable
|
||||||
onKeyDown={(e) => handleKeyDownLinea(e, linea.id)}
|
role="textbox"
|
||||||
onBlur={(e) => handleBlurLinea(e, linea.id)}
|
tabIndex={0}
|
||||||
onClick={() => {
|
aria-label={`Nombre de línea ${linea.nombre}`}
|
||||||
if (editingLineaId !== linea.id) {
|
suppressContentEditableWarning
|
||||||
setEditingLineaId(linea.id)
|
spellCheck={false}
|
||||||
setTempNombreLinea(linea.nombre)
|
onFocus={() => setEditingLineaId(linea.id)}
|
||||||
}
|
onKeyDown={(e) => {
|
||||||
}}
|
if (e.key === 'Enter') {
|
||||||
className={`block w-full truncate text-xs font-bold break-words outline-none ${
|
e.preventDefault()
|
||||||
editingLineaId === linea.id
|
e.currentTarget.blur()
|
||||||
? 'cursor-text border-b border-teal-500/50 pb-1'
|
}
|
||||||
: 'cursor-pointer'
|
if (e.key === 'Escape') {
|
||||||
}`}
|
e.preventDefault()
|
||||||
>
|
e.currentTarget.textContent = linea.nombre
|
||||||
{linea.nombre}
|
e.currentTarget.blur()
|
||||||
</span>
|
}
|
||||||
|
}}
|
||||||
|
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}
|
||||||
|
</span>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="top" className="max-w-xs text-sm">
|
||||||
|
{linea.nombre}
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-2 flex flex-shrink-0 items-center gap-1">
|
<div className="ml-1 flex shrink-0 items-center gap-1">
|
||||||
<button
|
<button
|
||||||
onClick={() => setEditingLineaId(linea.id)}
|
type="button"
|
||||||
className="..."
|
aria-label="Eliminar línea"
|
||||||
>
|
|
||||||
{' '}
|
|
||||||
<Pencil size={12} />{' '}
|
|
||||||
</button>
|
|
||||||
<Trash2
|
|
||||||
onClick={() => borrarLinea(linea.id)}
|
onClick={() => borrarLinea(linea.id)}
|
||||||
className="..."
|
className="text-destructive/80 hover:text-destructive hover:bg-destructive/10 inline-flex h-8 w-8 items-center justify-center rounded-md transition-colors"
|
||||||
size={14}
|
>
|
||||||
/>
|
<Trash2 size={14} />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{ciclosArray.map((ciclo) => (
|
{ciclosArray.map((cicloNumero) => (
|
||||||
<div
|
<div
|
||||||
key={`${linea.id}-${ciclo}`}
|
key={`${linea.id}-${cicloNumero}`}
|
||||||
onDragOver={handleDragOver}
|
onDragOver={handleDragOver}
|
||||||
onDrop={(e) => handleDrop(e, ciclo, linea.id)}
|
onDrop={(e) => handleDrop(e, cicloNumero, linea.id)}
|
||||||
className="min-h-35 space-y-2 rounded-xl border-2 border-dashed border-slate-100 bg-slate-50/20"
|
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
|
{asignaturas
|
||||||
.filter(
|
.filter(
|
||||||
(m) =>
|
(m) =>
|
||||||
m.ciclo === ciclo && m.lineaCurricularId === linea.id,
|
m.ciclo === cicloNumero &&
|
||||||
|
m.lineaCurricularId === linea.id,
|
||||||
)
|
)
|
||||||
.map((m) => (
|
.map((m) => (
|
||||||
<AsignaturaCardItem
|
<AsignaturaCardItem
|
||||||
@@ -860,21 +848,21 @@ function MapaCurricularPage() {
|
|||||||
))}
|
))}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={`flex flex-col justify-center rounded-xl border p-4 text-[10px] font-medium ${
|
className={`flex flex-col justify-center rounded-xl border p-4 text-[11px] font-medium ${
|
||||||
sub.cr === 0 && sub.hd === 0 && sub.hi === 0
|
sub.cr === 0 && sub.hd === 0 && sub.hi === 0
|
||||||
? 'border-slate-100 bg-slate-50/50 text-slate-300'
|
? 'border-border/50 bg-muted/20 text-muted-foreground/70'
|
||||||
: 'border-slate-200 bg-slate-50 text-slate-600'
|
: 'border-border bg-card text-muted-foreground'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{sub.cr === 0 && sub.hd === 0 && sub.hi === 0 ? (
|
{sub.cr === 0 && sub.hd === 0 && sub.hi === 0 ? (
|
||||||
<div className="text-slate-400">—</div>
|
<div className="text-muted-foreground">—</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<div className="font-bold text-slate-700">
|
<div className="text-foreground font-bold">
|
||||||
Cr: {sub.cr}
|
Cr: {sub.cr}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-slate-600">
|
<div>
|
||||||
HD: {sub.hd} • HI: {sub.hi}
|
HD: {sub.hd} • HI: {sub.hi}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -885,28 +873,30 @@ function MapaCurricularPage() {
|
|||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
|
||||||
<div className="col-span-full my-2 border-t border-slate-200"></div>
|
<div className="border-border col-span-full my-2 border-t"></div>
|
||||||
|
|
||||||
<div className="self-center p-2 font-bold text-slate-600">
|
<div className="text-foreground self-center p-2 font-bold">
|
||||||
Totales por Ciclo
|
Totales por Ciclo
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{ciclosArray.map((ciclo) => {
|
{ciclosArray.map((cicloNumero) => {
|
||||||
const t = getTotalesCiclo(ciclo)
|
const t = getTotalesCiclo(cicloNumero)
|
||||||
const isEmpty = t.cr === 0 && t.hd === 0 && t.hi === 0
|
const isEmpty = t.cr === 0 && t.hd === 0 && t.hi === 0
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={`footer-${ciclo}`}
|
key={`footer-${cicloNumero}`}
|
||||||
className={`rounded-lg p-2 text-center text-[10px] ${
|
className={`rounded-xl border p-2 text-center text-[11px] ${
|
||||||
isEmpty ? 'bg-slate-100/50 text-slate-400' : 'bg-slate-50'
|
isEmpty
|
||||||
|
? 'border-border/50 bg-muted/30 text-muted-foreground'
|
||||||
|
: 'border-border bg-card'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{isEmpty ? (
|
{isEmpty ? (
|
||||||
<div className="py-1 text-xs text-slate-400">—</div>
|
<div className="text-muted-foreground py-1 text-xs">—</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div className="font-bold text-slate-700">Cr: {t.cr}</div>
|
<div className="text-foreground font-bold">Cr: {t.cr}</div>
|
||||||
<div>
|
<div>
|
||||||
HD: {t.hd} • HI: {t.hi}
|
HD: {t.hd} • HI: {t.hi}
|
||||||
</div>
|
</div>
|
||||||
@@ -916,7 +906,7 @@ function MapaCurricularPage() {
|
|||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
|
||||||
<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="text-primary-foreground border-primary/40 bg-primary flex flex-col justify-center rounded-xl border p-2 text-center text-xs font-bold shadow-sm">
|
||||||
<div>{stats.cr} Cr</div>
|
<div>{stats.cr} Cr</div>
|
||||||
<div>{stats.hd + stats.hi} Hrs</div>
|
<div>{stats.hd + stats.hi} Hrs</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -960,8 +950,8 @@ function MapaCurricularPage() {
|
|||||||
onDragOver={handleDragOver}
|
onDragOver={handleDragOver}
|
||||||
onDrop={(e) => handleDrop(e, null, null)}
|
onDrop={(e) => handleDrop(e, null, null)}
|
||||||
className={[
|
className={[
|
||||||
'rounded-[24px] border-2 border-dashed p-4 transition-all duration-300',
|
'rounded-3xl border-2 border-dashed p-4 transition-all duration-300',
|
||||||
'min-h-[220px]',
|
'min-h-55',
|
||||||
draggedAsignatura
|
draggedAsignatura
|
||||||
? 'border-primary/35 bg-primary/6 shadow-inner'
|
? 'border-primary/35 bg-primary/6 shadow-inner'
|
||||||
: 'border-border bg-muted/20',
|
: 'border-border bg-muted/20',
|
||||||
@@ -970,7 +960,7 @@ function MapaCurricularPage() {
|
|||||||
{unassignedAsignaturas.length > 0 ? (
|
{unassignedAsignaturas.length > 0 ? (
|
||||||
<div className="flex flex-wrap gap-4">
|
<div className="flex flex-wrap gap-4">
|
||||||
{unassignedAsignaturas.map((m) => (
|
{unassignedAsignaturas.map((m) => (
|
||||||
<div key={m.id} className="w-[272px] shrink-0">
|
<div key={m.id} className="w-fit shrink-0">
|
||||||
<AsignaturaCardItem
|
<AsignaturaCardItem
|
||||||
asignatura={m}
|
asignatura={m}
|
||||||
lineaColor="#94A3B8"
|
lineaColor="#94A3B8"
|
||||||
@@ -986,7 +976,7 @@ function MapaCurricularPage() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="border-border/70 bg-background/70 flex min-h-[188px] flex-col items-center justify-center rounded-[20px] border px-6 text-center">
|
<div className="border-border/70 bg-background/70 flex min-h-47 flex-col items-center justify-center rounded-[20px] border px-6 text-center">
|
||||||
<div className="bg-muted text-muted-foreground mb-3 flex h-12 w-12 items-center justify-center rounded-2xl">
|
<div className="bg-muted text-muted-foreground mb-3 flex h-12 w-12 items-center justify-center rounded-2xl">
|
||||||
<Icons.CheckCheck className="h-5 w-5" />
|
<Icons.CheckCheck className="h-5 w-5" />
|
||||||
</div>
|
</div>
|
||||||
@@ -1007,11 +997,11 @@ function MapaCurricularPage() {
|
|||||||
{/* Modal de Edición */}
|
{/* Modal de Edición */}
|
||||||
<Dialog open={isEditModalOpen} onOpenChange={setIsEditModalOpen}>
|
<Dialog open={isEditModalOpen} onOpenChange={setIsEditModalOpen}>
|
||||||
<DialogContent
|
<DialogContent
|
||||||
className="sm:max-w-[550px]"
|
className="sm:max-w-137.5"
|
||||||
onInteractOutside={(e) => e.preventDefault()}
|
onInteractOutside={(e) => e.preventDefault()}
|
||||||
>
|
>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="font-bold text-slate-700">
|
<DialogTitle className="text-foreground font-bold">
|
||||||
Editar Asignatura
|
Editar Asignatura
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
@@ -1209,7 +1199,7 @@ function MapaCurricularPage() {
|
|||||||
)
|
)
|
||||||
.map((asig) => (
|
.map((asig) => (
|
||||||
<SelectItem key={asig.id} value={asig.id}>
|
<SelectItem key={asig.id} value={asig.id}>
|
||||||
<span className="font-bold text-teal-600">
|
<span className="text-primary font-bold">
|
||||||
[C{asig.ciclo}]
|
[C{asig.ciclo}]
|
||||||
</span>{' '}
|
</span>{' '}
|
||||||
{asig.nombre}
|
{asig.nombre}
|
||||||
@@ -1249,12 +1239,7 @@ function MapaCurricularPage() {
|
|||||||
>
|
>
|
||||||
Cancelar
|
Cancelar
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button onClick={handleSaveChanges}>Guardar</Button>
|
||||||
className="bg-teal-700 text-white"
|
|
||||||
onClick={handleSaveChanges}
|
|
||||||
>
|
|
||||||
Guardar
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
Reference in New Issue
Block a user