Merge pull request 'Que el renderizado no dependa de los query params' (#81) from issue/80-deshacerse-de-todos-estos-query-params-de-la-url into main
Reviewed-on: #81
This commit was merged in pull request #81.
This commit is contained in:
@@ -24,8 +24,6 @@ import { qk } from '@/data/query/keys'
|
|||||||
export const Route = createFileRoute('/planes/$planId/_detalle')({
|
export const Route = createFileRoute('/planes/$planId/_detalle')({
|
||||||
loader: async ({ context: { queryClient }, params: { planId } }) => {
|
loader: async ({ context: { queryClient }, params: { planId } }) => {
|
||||||
try {
|
try {
|
||||||
console.log('loader')
|
|
||||||
|
|
||||||
await queryClient.ensureQueryData({
|
await queryClient.ensureQueryData({
|
||||||
queryKey: qk.plan(planId),
|
queryKey: qk.plan(planId),
|
||||||
queryFn: () => plans_get(planId),
|
queryFn: () => plans_get(planId),
|
||||||
@@ -33,8 +31,6 @@ export const Route = createFileRoute('/planes/$planId/_detalle')({
|
|||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
// PGRST116: The result contains 0 rows
|
// PGRST116: The result contains 0 rows
|
||||||
if (e?.code === 'PGRST116') {
|
if (e?.code === 'PGRST116') {
|
||||||
console.log('not found on', Route.path)
|
|
||||||
|
|
||||||
throw notFound()
|
throw notFound()
|
||||||
}
|
}
|
||||||
throw e
|
throw e
|
||||||
@@ -80,31 +76,43 @@ function RouteComponent() {
|
|||||||
mutate({ planId, patch })
|
mutate({ planId, patch })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const MAX_CHARACTERS = 200
|
||||||
|
|
||||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLSpanElement>) => {
|
const handleKeyDown = (e: React.KeyboardEvent<HTMLSpanElement>) => {
|
||||||
|
// 1. Permitir teclas de control (Borrar, flechas, etc.) siempre
|
||||||
|
const isControlKey =
|
||||||
|
e.key === 'Backspace' ||
|
||||||
|
e.key === 'Delete' ||
|
||||||
|
e.key.includes('Arrow') ||
|
||||||
|
e.metaKey ||
|
||||||
|
e.ctrlKey
|
||||||
|
|
||||||
if (e.key === 'Enter') {
|
if (e.key === 'Enter') {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
e.currentTarget.blur() // Esto disparará el onBlur automáticamente
|
e.currentTarget.blur()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Bloquear si excede los 200 caracteres y no es una tecla de control
|
||||||
|
const currentText = e.currentTarget.textContent || ''
|
||||||
|
if (currentText.length >= MAX_CHARACTERS && !isControlKey) {
|
||||||
|
e.preventDefault()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleBlurNombre = (e: React.FocusEvent<HTMLSpanElement>) => {
|
const handlePaste = (e: React.ClipboardEvent<HTMLSpanElement>) => {
|
||||||
const nuevoNombre = e.currentTarget.textContent || ''
|
e.preventDefault()
|
||||||
setNombrePlan(nuevoNombre)
|
const text = e.clipboardData.getData('text/plain')
|
||||||
|
const currentText = e.currentTarget.textContent || ''
|
||||||
|
|
||||||
// Solo guardamos si el valor es realmente distinto al de la base de datos
|
// Calcular cuánto espacio queda
|
||||||
if (nuevoNombre !== data?.nombre) {
|
const remainingSpace = MAX_CHARACTERS - currentText.length
|
||||||
persistChange({ nombre: nuevoNombre })
|
|
||||||
|
if (remainingSpace > 0) {
|
||||||
|
const slicedText = text.slice(0, remainingSpace)
|
||||||
|
document.execCommand('insertText', false, slicedText)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSelectNivel = (n: string) => {
|
|
||||||
setNivelPlan(n)
|
|
||||||
// Guardamos inmediatamente al seleccionar
|
|
||||||
if (n !== data?.nivel) {
|
|
||||||
persistChange({ nivel: n })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-white">
|
<div className="min-h-screen bg-white">
|
||||||
{/* 1. Header Superior */}
|
{/* 1. Header Superior */}
|
||||||
@@ -131,8 +139,9 @@ function RouteComponent() {
|
|||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col items-start justify-between gap-4 md:flex-row">
|
<div className="flex flex-col items-start justify-between gap-4 md:flex-row">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="flex items-baseline gap-2 text-3xl font-bold tracking-tight text-slate-900">
|
<h1 className="flex flex-wrap items-baseline gap-2 text-3xl leading-tight font-bold tracking-tight text-slate-900">
|
||||||
<span>{nivelPlan} en</span>
|
{/* El prefijo "Nivel en" lo mantenemos simple */}
|
||||||
|
<span className="shrink-0">{nivelPlan} en</span>
|
||||||
<span
|
<span
|
||||||
role="textbox"
|
role="textbox"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
@@ -140,14 +149,17 @@ function RouteComponent() {
|
|||||||
suppressContentEditableWarning
|
suppressContentEditableWarning
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
|
onPaste={handlePaste} // Añadido para controlar lo que pegan
|
||||||
onBlur={(e) => {
|
onBlur={(e) => {
|
||||||
const nuevoNombre = e.currentTarget.textContent || ''
|
const nuevoNombre =
|
||||||
|
e.currentTarget.textContent?.trim() || ''
|
||||||
setNombrePlan(nuevoNombre)
|
setNombrePlan(nuevoNombre)
|
||||||
if (nuevoNombre !== data?.nombre) {
|
if (nuevoNombre !== data?.nombre) {
|
||||||
mutate({ planId, patch: { nombre: nuevoNombre } })
|
mutate({ planId, patch: { nombre: nuevoNombre } })
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className="cursor-text border-b border-transparent transition-colors outline-none select-text hover:border-slate-300 focus:border-teal-500"
|
// Clases añadidas: break-words y whitespace-pre-wrap para el wrap
|
||||||
|
className="block w-full cursor-text border-b border-transparent break-words whitespace-pre-wrap transition-colors outline-none select-text hover:border-slate-300 focus:border-teal-500 sm:inline-block sm:w-auto"
|
||||||
style={{ textDecoration: 'none' }}
|
style={{ textDecoration: 'none' }}
|
||||||
>
|
>
|
||||||
{nombrePlan}
|
{nombrePlan}
|
||||||
@@ -219,11 +231,7 @@ function RouteComponent() {
|
|||||||
<Tab to="/planes/$planId/" params={{ planId }}>
|
<Tab to="/planes/$planId/" params={{ planId }}>
|
||||||
Datos Generales
|
Datos Generales
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab
|
<Tab to="/planes/$planId/mapa" params={{ planId }}>
|
||||||
to="/planes/$planId/mapa"
|
|
||||||
params={{ planId }}
|
|
||||||
search={{ ciclo: data?.numero_ciclos }}
|
|
||||||
>
|
|
||||||
Mapa Curricular
|
Mapa Curricular
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab to="/planes/$planId/asignaturas" params={{ planId }}>
|
<Tab to="/planes/$planId/asignaturas" params={{ planId }}>
|
||||||
@@ -238,13 +246,7 @@ function RouteComponent() {
|
|||||||
<Tab to="/planes/$planId/documento" params={{ planId }}>
|
<Tab to="/planes/$planId/documento" params={{ planId }}>
|
||||||
Documento
|
Documento
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab
|
<Tab to="/planes/$planId/historial" params={{ planId }}>
|
||||||
to="/planes/$planId/historial"
|
|
||||||
params={{ planId }}
|
|
||||||
search={{
|
|
||||||
structure: data?.estructuras_plan?.definicion?.properties,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Historial
|
Historial
|
||||||
</Tab>
|
</Tab>
|
||||||
</nav>
|
</nav>
|
||||||
@@ -298,7 +300,6 @@ const InfoCard = forwardRef<
|
|||||||
function Tab({
|
function Tab({
|
||||||
to,
|
to,
|
||||||
params,
|
params,
|
||||||
search,
|
|
||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
to: string
|
to: string
|
||||||
@@ -306,12 +307,10 @@ function Tab({
|
|||||||
search?: any
|
search?: any
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
}) {
|
}) {
|
||||||
console.log(search)
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
to={to}
|
to={to}
|
||||||
params={params}
|
params={params}
|
||||||
search={search}
|
|
||||||
className="border-b-2 border-transparent pb-3 text-sm font-medium text-slate-500 transition-all hover:text-slate-800"
|
className="border-b-2 border-transparent pb-3 text-sm font-medium text-slate-500 transition-all hover:text-slate-800"
|
||||||
activeProps={{ className: 'border-teal-600 text-teal-700 font-bold' }}
|
activeProps={{ className: 'border-teal-600 text-teal-700 font-bold' }}
|
||||||
activeOptions={{
|
activeOptions={{
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
History,
|
History,
|
||||||
Calendar,
|
Calendar,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { useMemo, useState } from 'react'
|
import { useEffect, useMemo, useState } from 'react'
|
||||||
|
|
||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
import { Card, CardContent } from '@/components/ui/card'
|
import { Card, CardContent } from '@/components/ui/card'
|
||||||
@@ -23,13 +23,10 @@ import {
|
|||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from '@/components/ui/dialog'
|
} from '@/components/ui/dialog'
|
||||||
import { usePlanHistorial } from '@/data/hooks/usePlans'
|
import { usePlan, usePlanHistorial } from '@/data/hooks/usePlans'
|
||||||
|
|
||||||
export const Route = createFileRoute('/planes/$planId/_detalle/historial')({
|
export const Route = createFileRoute('/planes/$planId/_detalle/historial')({
|
||||||
component: RouteComponent,
|
component: RouteComponent,
|
||||||
validateSearch: (search: { structure?: any }) => ({
|
|
||||||
structure: search.structure ?? null,
|
|
||||||
}),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const getEventConfig = (tipo: string, campo: string) => {
|
const getEventConfig = (tipo: string, campo: string) => {
|
||||||
@@ -61,14 +58,23 @@ const getEventConfig = (tipo: string, campo: string) => {
|
|||||||
function RouteComponent() {
|
function RouteComponent() {
|
||||||
const { planId } = Route.useParams()
|
const { planId } = Route.useParams()
|
||||||
const { data: rawData, isLoading } = usePlanHistorial(planId)
|
const { data: rawData, isLoading } = usePlanHistorial(planId)
|
||||||
const { structure } = Route.useSearch()
|
const [structure, setStructure] = useState<any>(null)
|
||||||
console.log(structure?.vigencia?.title)
|
const { data } = usePlan(planId)
|
||||||
console.log(structure)
|
console.log('analizando estructura')
|
||||||
|
|
||||||
|
console.log(data?.estructuras_plan?.definicion?.properties)
|
||||||
|
// console.log(structure)
|
||||||
|
|
||||||
// ESTADOS PARA EL MODAL
|
// ESTADOS PARA EL MODAL
|
||||||
const [selectedEvent, setSelectedEvent] = useState<any>(null)
|
const [selectedEvent, setSelectedEvent] = useState<any>(null)
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false)
|
const [isModalOpen, setIsModalOpen] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data?.estructuras_plan?.definicion?.properties) {
|
||||||
|
setStructure(data.estructuras_plan.definicion.properties)
|
||||||
|
}
|
||||||
|
}, [data])
|
||||||
|
|
||||||
const historyEvents = useMemo(() => {
|
const historyEvents = useMemo(() => {
|
||||||
if (!rawData) return []
|
if (!rawData) return []
|
||||||
return rawData.map((item: any) => {
|
return rawData.map((item: any) => {
|
||||||
|
|||||||
@@ -93,7 +93,6 @@ function DatosGeneralesPage() {
|
|||||||
|
|
||||||
requerido: true,
|
requerido: true,
|
||||||
|
|
||||||
// 👇 TIPO DE CAMPO
|
|
||||||
tipo: Array.isArray(schema?.enum)
|
tipo: Array.isArray(schema?.enum)
|
||||||
? 'select'
|
? 'select'
|
||||||
: schema?.type === 'number'
|
: schema?.type === 'number'
|
||||||
@@ -107,8 +106,6 @@ function DatosGeneralesPage() {
|
|||||||
|
|
||||||
setCampos(datosTransformados)
|
setCampos(datosTransformados)
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(properties)
|
|
||||||
}, [data])
|
}, [data])
|
||||||
|
|
||||||
// 3. Manejadores de acciones (Ahora como funciones locales)
|
// 3. Manejadores de acciones (Ahora como funciones locales)
|
||||||
@@ -220,8 +217,6 @@ function DatosGeneralesPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleIARequest = (clave: string) => {
|
const handleIARequest = (clave: string) => {
|
||||||
console.log(clave)
|
|
||||||
|
|
||||||
navigate({
|
navigate({
|
||||||
to: '/planes/$planId/iaplan',
|
to: '/planes/$planId/iaplan',
|
||||||
params: {
|
params: {
|
||||||
@@ -244,9 +239,8 @@ function DatosGeneralesPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 gap-6 lg:grid-cols-2">
|
<div className="grid grid-cols-1 gap-6 lg:grid-cols-2">
|
||||||
{campos.map((campo, key) => {
|
{campos.map((campo) => {
|
||||||
const isEditing = editingId === campo.id
|
const isEditing = editingId === campo.id
|
||||||
console.log(campo)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
useCreateLinea,
|
useCreateLinea,
|
||||||
useDeleteLinea,
|
useDeleteLinea,
|
||||||
|
usePlan,
|
||||||
usePlanAsignaturas,
|
usePlanAsignaturas,
|
||||||
usePlanLineas,
|
usePlanLineas,
|
||||||
useUpdateAsignatura,
|
useUpdateAsignatura,
|
||||||
@@ -166,44 +167,39 @@ function AsignaturaCardItem({
|
|||||||
|
|
||||||
export const Route = createFileRoute('/planes/$planId/_detalle/mapa')({
|
export const Route = createFileRoute('/planes/$planId/_detalle/mapa')({
|
||||||
component: MapaCurricularPage,
|
component: MapaCurricularPage,
|
||||||
validateSearch: (search: { ciclo?: number }) => ({
|
|
||||||
ciclo: search.ciclo ?? null,
|
|
||||||
}),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
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 { ciclo } = Route.useSearch()
|
const { data } = usePlan(planId)
|
||||||
|
const [ciclo, setCiclo] = useState(0)
|
||||||
const [editingLineaId, setEditingLineaId] = useState<string | null>(null)
|
const [editingLineaId, setEditingLineaId] = useState<string | null>(null)
|
||||||
const [tempNombreLinea, setTempNombreLinea] = useState('')
|
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()
|
||||||
// 1. Fetch de Datos
|
|
||||||
const { data: asignaturasApi, isLoading: loadingAsig } =
|
const { data: asignaturasApi, isLoading: loadingAsig } =
|
||||||
usePlanAsignaturas(planId)
|
usePlanAsignaturas(planId)
|
||||||
const { data: lineasApi, isLoading: loadingLineas } = usePlanLineas(planId)
|
const { data: lineasApi, isLoading: loadingLineas } = usePlanLineas(planId)
|
||||||
|
|
||||||
// 2. Estado Local (Para interactividad)
|
|
||||||
const [asignaturas, setAsignaturas] = useState<Array<Asignatura>>([])
|
const [asignaturas, setAsignaturas] = useState<Array<Asignatura>>([])
|
||||||
const [lineas, setLineas] = useState<Array<LineaCurricular>>([])
|
const [lineas, setLineas] = useState<Array<LineaCurricular>>([])
|
||||||
const [draggedAsignatura, setDraggedAsignatura] = useState<string | null>(
|
const [draggedAsignatura, setDraggedAsignatura] = useState<string | null>(
|
||||||
null,
|
null,
|
||||||
)
|
)
|
||||||
const [isEditModalOpen, setIsEditModalOpen] = useState(false)
|
const [isEditModalOpen, setIsEditModalOpen] = useState(false)
|
||||||
const [selectedAsignatura, setSelectedAsignatura] =
|
|
||||||
useState<Asignatura | null>(null)
|
|
||||||
const [hasAreaComun, setHasAreaComun] = 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, isPending } = useUpdateAsignatura()
|
const { mutate: updateAsignatura } = useUpdateAsignatura()
|
||||||
|
const [seriacionValue, setSeriacionValue] = useState<string>('unassigned')
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data?.numero_ciclos) {
|
||||||
|
setCiclo(data.numero_ciclos)
|
||||||
|
}
|
||||||
|
}, [data])
|
||||||
|
|
||||||
const manejarAgregarLinea = (nombre: string) => {
|
const manejarAgregarLinea = (nombre: string) => {
|
||||||
const nombreNormalizado = nombre.trim()
|
const nombreNormalizado = nombre.trim()
|
||||||
|
|
||||||
// 1. Validar vacío
|
|
||||||
if (!nombreNormalizado) return
|
if (!nombreNormalizado) return
|
||||||
|
|
||||||
// 2. Validar duplicados en el estado local (Insensible a mayúsculas/acentos)
|
|
||||||
const nombreBusqueda = nombreNormalizado
|
const nombreBusqueda = nombreNormalizado
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.normalize('NFD')
|
.normalize('NFD')
|
||||||
@@ -219,12 +215,9 @@ function MapaCurricularPage() {
|
|||||||
|
|
||||||
if (yaExiste) {
|
if (yaExiste) {
|
||||||
alert(`La línea "${nombreNormalizado}" ya existe en este plan.`)
|
alert(`La línea "${nombreNormalizado}" ya existe en este plan.`)
|
||||||
return // DETIENE la ejecución aquí, no llega a la mutación
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Si pasa las validaciones, procedemos con la persistencia
|
|
||||||
const maxOrden = lineas.reduce((max, l) => Math.max(max, l.orden || 0), 0)
|
const maxOrden = lineas.reduce((max, l) => Math.max(max, l.orden || 0), 0)
|
||||||
|
|
||||||
createLinea(
|
createLinea(
|
||||||
{
|
{
|
||||||
nombre: nombreNormalizado,
|
nombre: nombreNormalizado,
|
||||||
@@ -234,7 +227,6 @@ function MapaCurricularPage() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
onSuccess: (nueva) => {
|
onSuccess: (nueva) => {
|
||||||
// Sincronización local que ya teníamos
|
|
||||||
const mapeada = {
|
const mapeada = {
|
||||||
id: nueva.id,
|
id: nueva.id,
|
||||||
nombre: nueva.nombre,
|
nombre: nueva.nombre,
|
||||||
@@ -280,7 +272,6 @@ function MapaCurricularPage() {
|
|||||||
)
|
)
|
||||||
}, [lineas])
|
}, [lineas])
|
||||||
|
|
||||||
// 3. Sincronizar API -> Estado Local
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (asignaturasApi)
|
if (asignaturasApi)
|
||||||
setAsignaturas(mapAsignaturasToAsignaturas(asignaturasApi))
|
setAsignaturas(mapAsignaturasToAsignaturas(asignaturasApi))
|
||||||
@@ -292,20 +283,37 @@ function MapaCurricularPage() {
|
|||||||
|
|
||||||
const ciclosTotales = Number(ciclo)
|
const ciclosTotales = Number(ciclo)
|
||||||
const ciclosArray = Array.from({ length: ciclosTotales }, (_, i) => i + 1)
|
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<Asignatura | null>(null)
|
const [editingData, setEditingData] = useState<Asignatura | null>(null)
|
||||||
|
const handleIntegerChange = (value: string) => {
|
||||||
|
if (value === '') return value
|
||||||
|
|
||||||
// 1. FUNCION DE GUARDAR MODAL
|
// Solo números, máximo 3 cifras
|
||||||
|
const regex = /^\d{1,3}$/
|
||||||
|
|
||||||
|
if (!regex.test(value)) return null
|
||||||
|
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
const handleDecimalChange = (value: string, max?: number): string | null => {
|
||||||
|
if (value === '') return ''
|
||||||
|
|
||||||
|
const val = value.replace(',', '.')
|
||||||
|
const regex = /^\d*\.?\d{0,2}$/
|
||||||
|
if (!regex.test(val)) return null
|
||||||
|
if (max !== undefined) {
|
||||||
|
const num = Number(val)
|
||||||
|
if (!isNaN(num) && num > max) {
|
||||||
|
return max.toFixed(2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return val
|
||||||
|
}
|
||||||
const handleSaveChanges = () => {
|
const handleSaveChanges = () => {
|
||||||
if (!editingData) return
|
if (!editingData) return
|
||||||
console.log(asignaturas)
|
|
||||||
|
|
||||||
setAsignaturas((prev) =>
|
setAsignaturas((prev) =>
|
||||||
prev.map((m) => (m.id === editingData.id ? { ...editingData } : m)),
|
prev.map((m) => (m.id === editingData.id ? { ...editingData } : m)),
|
||||||
)
|
)
|
||||||
// setIsEditModalOpen(false)
|
|
||||||
// Preparamos el patch con la estructura de tu tabla
|
|
||||||
const patch = {
|
const patch = {
|
||||||
nombre: editingData.nombre,
|
nombre: editingData.nombre,
|
||||||
codigo: editingData.clave,
|
codigo: editingData.clave,
|
||||||
@@ -332,22 +340,11 @@ function MapaCurricularPage() {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. MODIFICACIÓN: Zona de soltado siempre visible
|
|
||||||
// Cambiamos la condición: Mostramos la sección si hay asignaturas sin asignar
|
|
||||||
// O si simplemente queremos tener el "depósito" disponible.
|
|
||||||
const unassignedAsignaturas = asignaturas.filter(
|
const unassignedAsignaturas = asignaturas.filter(
|
||||||
(m) => m.ciclo === null || m.lineaCurricularId === null,
|
(m) => m.ciclo === null || m.lineaCurricularId === null,
|
||||||
)
|
)
|
||||||
|
|
||||||
// --- Lógica de Gestión ---
|
|
||||||
const agregarLinea = (nombre: string) => {
|
|
||||||
const nueva = { id: crypto.randomUUID(), nombre, orden: lineas.length + 1 }
|
|
||||||
setLineas([...lineas, nueva])
|
|
||||||
}
|
|
||||||
|
|
||||||
const borrarLinea = (id: string) => {
|
const borrarLinea = (id: string) => {
|
||||||
// 1. Opcional: Confirmación de seguridad
|
|
||||||
if (
|
if (
|
||||||
!confirm(
|
!confirm(
|
||||||
'¿Estás seguro de eliminar esta línea? Las materias asignadas volverán a la bandeja de entrada.',
|
'¿Estás seguro de eliminar esta línea? Las materias asignadas volverán a la bandeja de entrada.',
|
||||||
@@ -356,11 +353,8 @@ function MapaCurricularPage() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Llamada a la API
|
|
||||||
deleteLineaApi(id, {
|
deleteLineaApi(id, {
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
// 3. Actualización instantánea del estado local
|
|
||||||
|
|
||||||
// Primero: Las materias que estaban en esa línea pasan a ser "huérfanas"
|
// Primero: Las materias que estaban en esa línea pasan a ser "huérfanas"
|
||||||
setAsignaturas((prev) =>
|
setAsignaturas((prev) =>
|
||||||
prev.map((asig) =>
|
prev.map((asig) =>
|
||||||
@@ -369,8 +363,6 @@ function MapaCurricularPage() {
|
|||||||
: asig,
|
: asig,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
// Segundo: Quitamos la línea del estado
|
|
||||||
setLineas((prev) => prev.filter((l) => l.id !== id))
|
setLineas((prev) => prev.filter((l) => l.id !== id))
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
@@ -427,8 +419,6 @@ function MapaCurricularPage() {
|
|||||||
: m,
|
: m,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
// 2. Persistir en la API
|
|
||||||
const patch = {
|
const patch = {
|
||||||
numero_ciclo: ciclo,
|
numero_ciclo: ciclo,
|
||||||
linea_plan_id: lineaId,
|
linea_plan_id: lineaId,
|
||||||
@@ -582,7 +572,6 @@ function MapaCurricularPage() {
|
|||||||
>
|
>
|
||||||
{editingLineaId === linea.id ? (
|
{editingLineaId === linea.id ? (
|
||||||
<Input
|
<Input
|
||||||
autoFocus
|
|
||||||
className="h-7 bg-white text-xs"
|
className="h-7 bg-white text-xs"
|
||||||
value={tempNombreLinea}
|
value={tempNombreLinea}
|
||||||
onChange={(e) => setTempNombreLinea(e.target.value)}
|
onChange={(e) => setTempNombreLinea(e.target.value)}
|
||||||
@@ -592,6 +581,7 @@ function MapaCurricularPage() {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
|
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
|
||||||
<span
|
<span
|
||||||
className="cursor-pointer text-xs font-bold hover:underline"
|
className="cursor-pointer text-xs font-bold hover:underline"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -747,6 +737,7 @@ function MapaCurricularPage() {
|
|||||||
Clave
|
Clave
|
||||||
</label>
|
</label>
|
||||||
<Input
|
<Input
|
||||||
|
maxLength={100}
|
||||||
value={editingData.clave}
|
value={editingData.clave}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setEditingData({ ...editingData, clave: e.target.value })
|
setEditingData({ ...editingData, clave: e.target.value })
|
||||||
@@ -758,6 +749,7 @@ function MapaCurricularPage() {
|
|||||||
Nombre
|
Nombre
|
||||||
</label>
|
</label>
|
||||||
<Input
|
<Input
|
||||||
|
maxLength={200}
|
||||||
value={editingData.nombre}
|
value={editingData.nombre}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setEditingData({ ...editingData, nombre: e.target.value })
|
setEditingData({ ...editingData, nombre: e.target.value })
|
||||||
@@ -774,13 +766,17 @@ function MapaCurricularPage() {
|
|||||||
</label>
|
</label>
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
|
min={0}
|
||||||
value={editingData.creditos}
|
value={editingData.creditos}
|
||||||
onChange={(e) =>
|
onChange={(e) => {
|
||||||
setEditingData({
|
const val = handleDecimalChange(e.target.value, 10)
|
||||||
...editingData,
|
if (val !== null) {
|
||||||
creditos: Number(e.target.value),
|
setEditingData({
|
||||||
})
|
...editingData,
|
||||||
}
|
creditos: val === '' ? 0 : Number(val),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
@@ -790,12 +786,15 @@ function MapaCurricularPage() {
|
|||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
value={editingData.hd}
|
value={editingData.hd}
|
||||||
onChange={(e) =>
|
onChange={(e) => {
|
||||||
setEditingData({
|
const val = handleIntegerChange(e.target.value)
|
||||||
...editingData,
|
if (val !== null) {
|
||||||
hd: Number(e.target.value),
|
setEditingData({
|
||||||
})
|
...editingData,
|
||||||
}
|
hd: Number(e.target.value),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
@@ -805,12 +804,15 @@ function MapaCurricularPage() {
|
|||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
value={editingData.hi}
|
value={editingData.hi}
|
||||||
onChange={(e) =>
|
onChange={(e) => {
|
||||||
setEditingData({
|
const val = handleIntegerChange(e.target.value)
|
||||||
...editingData,
|
if (val !== null) {
|
||||||
hi: Number(e.target.value),
|
setEditingData({
|
||||||
})
|
...editingData,
|
||||||
}
|
hi: Number(e.target.value),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -882,23 +884,29 @@ function MapaCurricularPage() {
|
|||||||
Seriación (Prerrequisitos)
|
Seriación (Prerrequisitos)
|
||||||
</label>
|
</label>
|
||||||
<Select
|
<Select
|
||||||
|
value={seriacionValue}
|
||||||
onValueChange={(val) => {
|
onValueChange={(val) => {
|
||||||
// Evitamos duplicados en la lista de prerrequisitos local
|
if (val === 'unassigned') {
|
||||||
|
setSeriacionValue('unassigned')
|
||||||
|
return
|
||||||
|
}
|
||||||
if (!editingData.prerrequisitos.includes(val)) {
|
if (!editingData.prerrequisitos.includes(val)) {
|
||||||
setEditingData({
|
setEditingData({
|
||||||
...editingData,
|
...editingData,
|
||||||
prerrequisitos: [...editingData.prerrequisitos, val],
|
prerrequisitos: [...editingData.prerrequisitos, val],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
setSeriacionValue('unassigned')
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder="Seleccionar asignatura..." />
|
<SelectValue placeholder="Seleccionar asignatura..." />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{/* FILTRO CLAVE:
|
<SelectItem value="unassigned">
|
||||||
Solo mostramos materias cuyo ID sea diferente al de la materia que estamos editando
|
-- Sin Seriación --
|
||||||
*/}
|
</SelectItem>
|
||||||
|
|
||||||
{asignaturas
|
{asignaturas
|
||||||
.filter((m) => m.id !== editingData.id)
|
.filter((m) => m.id !== editingData.id)
|
||||||
.map((m) => (
|
.map((m) => (
|
||||||
|
|||||||
Reference in New Issue
Block a user