Merge pull request 'En el mapa curricular editar los nombres de las líneas curriculares #57' (#65) from issue/57-en-el-mapa-curricular-editar-los-nombres-de-las-ln into main
Reviewed-on: #65
This commit was merged in pull request #65.
This commit is contained in:
@@ -243,3 +243,52 @@ export async function asignaturas_update(
|
|||||||
throwIfError(error)
|
throwIfError(error)
|
||||||
return requireData(data, 'No se pudo actualizar la asignatura.')
|
return requireData(data, 'No se pudo actualizar la asignatura.')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Insertar una nueva línea
|
||||||
|
export async function lineas_insert(linea: {
|
||||||
|
nombre: string
|
||||||
|
plan_estudio_id: string
|
||||||
|
orden: number
|
||||||
|
area?: string
|
||||||
|
}) {
|
||||||
|
const supabase = supabaseBrowser()
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from('lineas_plan') // Asegúrate que el nombre de la tabla sea correcto
|
||||||
|
.insert([linea])
|
||||||
|
.select()
|
||||||
|
.single()
|
||||||
|
|
||||||
|
if (error) throw error
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
// Actualizar una línea existente
|
||||||
|
export async function lineas_update(
|
||||||
|
lineaId: string,
|
||||||
|
patch: { nombre?: string; orden?: number; area?: string },
|
||||||
|
) {
|
||||||
|
const supabase = supabaseBrowser()
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from('lineas_plan')
|
||||||
|
.update(patch)
|
||||||
|
.eq('id', lineaId)
|
||||||
|
.select()
|
||||||
|
.single()
|
||||||
|
|
||||||
|
if (error) throw error
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function lineas_delete(lineaId: string) {
|
||||||
|
const supabase = supabaseBrowser()
|
||||||
|
|
||||||
|
// Nota: Si configuraste "ON DELETE SET NULL" en tu base de datos,
|
||||||
|
// las asignaturas se desvincularán solas. Si no, Supabase podría dar error.
|
||||||
|
const { error } = await supabase
|
||||||
|
.from('lineas_plan')
|
||||||
|
.delete()
|
||||||
|
.eq('id', lineaId)
|
||||||
|
|
||||||
|
if (error) throw error
|
||||||
|
return lineaId
|
||||||
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import {
|
|||||||
plans_update_fields,
|
plans_update_fields,
|
||||||
plans_update_map,
|
plans_update_map,
|
||||||
} from '../api/plans.api'
|
} from '../api/plans.api'
|
||||||
|
import { lineas_delete } from '../api/subjects.api'
|
||||||
import { qk } from '../query/keys'
|
import { qk } from '../query/keys'
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
@@ -257,3 +258,15 @@ export function useGeneratePlanDocumento() {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useDeleteLinea() {
|
||||||
|
const qc = useQueryClient()
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: lineas_delete,
|
||||||
|
onSuccess: (idEliminado) => {
|
||||||
|
// Invalidamos para que las materias y líneas se refresquen
|
||||||
|
qc.invalidateQueries({ queryKey: ['plan_lineas'] })
|
||||||
|
qc.invalidateQueries({ queryKey: ['plan_asignaturas'] })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
|||||||
import {
|
import {
|
||||||
ai_generate_subject,
|
ai_generate_subject,
|
||||||
asignaturas_update,
|
asignaturas_update,
|
||||||
|
lineas_insert,
|
||||||
|
lineas_update,
|
||||||
subjects_bibliografia_list,
|
subjects_bibliografia_list,
|
||||||
subjects_clone_from_existing,
|
subjects_clone_from_existing,
|
||||||
subjects_create_manual,
|
subjects_create_manual,
|
||||||
@@ -222,3 +224,28 @@ export function useUpdateAsignatura() {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useCreateLinea() {
|
||||||
|
const qc = useQueryClient()
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: lineas_insert,
|
||||||
|
onSuccess: (nuevaLinea) => {
|
||||||
|
qc.invalidateQueries({
|
||||||
|
queryKey: ['plan_lineas', nuevaLinea.plan_estudio_id],
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useUpdateLinea() {
|
||||||
|
const qc = useQueryClient()
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (vars: { lineaId: string; patch: any }) =>
|
||||||
|
lineas_update(vars.lineaId, vars.patch),
|
||||||
|
onSuccess: (updated) => {
|
||||||
|
qc.invalidateQueries({
|
||||||
|
queryKey: ['plan_lineas', updated.plan_estudio_id],
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable jsx-a11y/click-events-have-key-events */
|
||||||
/* 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 {
|
import {
|
||||||
@@ -33,7 +34,14 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@/components/ui/select'
|
} from '@/components/ui/select'
|
||||||
import { usePlanAsignaturas, usePlanLineas, useUpdateAsignatura } from '@/data'
|
import {
|
||||||
|
useCreateLinea,
|
||||||
|
useDeleteLinea,
|
||||||
|
usePlanAsignaturas,
|
||||||
|
usePlanLineas,
|
||||||
|
useUpdateAsignatura,
|
||||||
|
useUpdateLinea,
|
||||||
|
} from '@/data'
|
||||||
|
|
||||||
// --- Mapeadores (Fuera del componente para mayor limpieza) ---
|
// --- Mapeadores (Fuera del componente para mayor limpieza) ---
|
||||||
const mapLineasToLineaCurricular = (
|
const mapLineasToLineaCurricular = (
|
||||||
@@ -166,8 +174,11 @@ 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 { ciclo } = Route.useSearch()
|
const { ciclo } = Route.useSearch()
|
||||||
console.log(ciclo)
|
const [editingLineaId, setEditingLineaId] = useState<string | null>(null)
|
||||||
|
const [tempNombreLinea, setTempNombreLinea] = useState('')
|
||||||
|
const { mutate: createLinea } = useCreateLinea()
|
||||||
|
const { mutate: updateLineaApi } = useUpdateLinea()
|
||||||
|
const { mutate: deleteLineaApi } = useDeleteLinea()
|
||||||
// 1. Fetch de Datos
|
// 1. Fetch de Datos
|
||||||
const { data: asignaturasApi, isLoading: loadingAsig } =
|
const { data: asignaturasApi, isLoading: loadingAsig } =
|
||||||
usePlanAsignaturas(planId)
|
usePlanAsignaturas(planId)
|
||||||
@@ -189,55 +200,78 @@ function MapaCurricularPage() {
|
|||||||
const manejarAgregarLinea = (nombre: string) => {
|
const manejarAgregarLinea = (nombre: string) => {
|
||||||
const nombreNormalizado = nombre.trim()
|
const nombreNormalizado = nombre.trim()
|
||||||
|
|
||||||
// 1. Validar que no esté vacío
|
// 1. Validar vacío
|
||||||
if (!nombreNormalizado) return
|
if (!nombreNormalizado) return
|
||||||
|
|
||||||
// 2. Validar duplicados (Insensible a mayúsculas/minúsculas y acentos)
|
// 2. Validar duplicados en el estado local (Insensible a mayúsculas/acentos)
|
||||||
const nombreParaComparar = nombreNormalizado
|
const nombreBusqueda = nombreNormalizado
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.normalize('NFD')
|
.normalize('NFD')
|
||||||
.replace(/[\u0300-\u036f]/g, '')
|
.replace(/[\u0300-\u036f]/g, '')
|
||||||
|
|
||||||
const yaExiste = lineas.some((l) => {
|
const yaExiste = lineas.some((l) => {
|
||||||
const lineaNombreBase = l.nombre
|
const lineaExistente = l.nombre
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.normalize('NFD')
|
.normalize('NFD')
|
||||||
.replace(/[\u0300-\u036f]/g, '')
|
.replace(/[\u0300-\u036f]/g, '')
|
||||||
return lineaNombreBase === nombreParaComparar
|
return lineaExistente === nombreBusqueda
|
||||||
})
|
})
|
||||||
|
|
||||||
if (yaExiste) {
|
if (yaExiste) {
|
||||||
alert(`La línea "${nombreNormalizado}" ya existe.`)
|
alert(`La línea "${nombreNormalizado}" ya existe en este plan.`)
|
||||||
return
|
return // DETIENE la ejecución aquí, no llega a la mutación
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Validar Área Común (usando tu lógica previa)
|
// 3. Si pasa las validaciones, procedemos con la persistencia
|
||||||
const esAreaComun =
|
const maxOrden = lineas.reduce((max, l) => Math.max(max, l.orden || 0), 0)
|
||||||
nombreNormalizado.toLowerCase() === 'área común' ||
|
|
||||||
nombreNormalizado.toLowerCase() === 'area comun'
|
|
||||||
|
|
||||||
if (esAreaComun && hasAreaComun) {
|
createLinea(
|
||||||
alert('El Área Común ya ha sido agregada.')
|
{
|
||||||
return
|
nombre: nombreNormalizado,
|
||||||
}
|
plan_estudio_id: planId,
|
||||||
|
orden: maxOrden + 1,
|
||||||
// 4. Agregar la línea si todo está bien
|
area: 'sin asignar',
|
||||||
const nueva = {
|
},
|
||||||
id: crypto.randomUUID(),
|
{
|
||||||
nombre: nombreNormalizado,
|
onSuccess: (nueva) => {
|
||||||
orden: lineas.length + 1,
|
// Sincronización local que ya teníamos
|
||||||
color: '#1976d2',
|
const mapeada = {
|
||||||
}
|
id: nueva.id,
|
||||||
|
nombre: nueva.nombre,
|
||||||
setLineas([...lineas, nueva])
|
orden: nueva.orden,
|
||||||
|
color: '#1976d2',
|
||||||
if (esAreaComun) {
|
}
|
||||||
setHasAreaComun(true)
|
setLineas((prev) => [...prev, mapeada])
|
||||||
}
|
setNombreNuevaLinea('')
|
||||||
|
},
|
||||||
setNombreNuevaLinea('') // Limpiar input
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
const guardarEdicionLinea = (id: string) => {
|
||||||
|
if (!tempNombreLinea.trim()) {
|
||||||
|
setEditingLineaId(null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
updateLineaApi(
|
||||||
|
{
|
||||||
|
lineaId: id,
|
||||||
|
patch: { nombre: tempNombreLinea.trim() },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: (lineaActualizada) => {
|
||||||
|
// ACTUALIZACIÓN MANUAL DEL ESTADO LOCAL
|
||||||
|
setLineas((prev) =>
|
||||||
|
prev.map((l) =>
|
||||||
|
l.id === id ? { ...l, nombre: lineaActualizada.nombre } : l,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
setEditingLineaId(null)
|
||||||
|
setTempNombreLinea('')
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
const tieneAreaComun = useMemo(() => {
|
const tieneAreaComun = useMemo(() => {
|
||||||
return lineas.some(
|
return lineas.some(
|
||||||
(l) =>
|
(l) =>
|
||||||
@@ -313,14 +347,37 @@ function MapaCurricularPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const borrarLinea = (id: string) => {
|
const borrarLinea = (id: string) => {
|
||||||
setAsignaturas((prev) =>
|
// 1. Opcional: Confirmación de seguridad
|
||||||
prev.map((m) =>
|
if (
|
||||||
m.lineaCurricularId === id
|
!confirm(
|
||||||
? { ...m, ciclo: null, lineaCurricularId: null }
|
'¿Estás seguro de eliminar esta línea? Las materias asignadas volverán a la bandeja de entrada.',
|
||||||
: m,
|
)
|
||||||
),
|
) {
|
||||||
)
|
return
|
||||||
setLineas((prev) => prev.filter((l) => l.id !== id))
|
}
|
||||||
|
|
||||||
|
// 2. Llamada a la API
|
||||||
|
deleteLineaApi(id, {
|
||||||
|
onSuccess: () => {
|
||||||
|
// 3. Actualización instantánea del estado local
|
||||||
|
|
||||||
|
// Primero: Las materias que estaban en esa línea pasan a ser "huérfanas"
|
||||||
|
setAsignaturas((prev) =>
|
||||||
|
prev.map((asig) =>
|
||||||
|
asig.lineaCurricularId === id
|
||||||
|
? { ...asig, ciclo: null, lineaCurricularId: null }
|
||||||
|
: asig,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Segundo: Quitamos la línea del estado
|
||||||
|
setLineas((prev) => prev.filter((l) => l.id !== id))
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
console.error(error)
|
||||||
|
alert('No se pudo eliminar la línea. Verifica si tiene dependencias.')
|
||||||
|
},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Selectores/Cálculos ---
|
// --- Selectores/Cálculos ---
|
||||||
@@ -523,11 +580,33 @@ function MapaCurricularPage() {
|
|||||||
<div
|
<div
|
||||||
className={`flex items-center justify-between rounded-xl border-l-4 p-4 ${lineColors[idx % lineColors.length]}`}
|
className={`flex items-center justify-between rounded-xl border-l-4 p-4 ${lineColors[idx % lineColors.length]}`}
|
||||||
>
|
>
|
||||||
<span className="text-xs font-bold">{linea.nombre}</span>
|
{editingLineaId === linea.id ? (
|
||||||
|
<Input
|
||||||
|
autoFocus
|
||||||
|
className="h-7 bg-white text-xs"
|
||||||
|
value={tempNombreLinea}
|
||||||
|
onChange={(e) => setTempNombreLinea(e.target.value)}
|
||||||
|
onBlur={() => guardarEdicionLinea(linea.id)}
|
||||||
|
onKeyDown={(e) =>
|
||||||
|
e.key === 'Enter' && guardarEdicionLinea(linea.id)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<span
|
||||||
|
className="cursor-pointer text-xs font-bold hover:underline"
|
||||||
|
onClick={() => {
|
||||||
|
setEditingLineaId(linea.id)
|
||||||
|
setTempNombreLinea(linea.nombre)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{linea.nombre}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
<Trash2
|
<Trash2
|
||||||
size={14}
|
size={14}
|
||||||
className="cursor-pointer text-slate-400 hover:text-red-500"
|
className="cursor-pointer text-slate-400 hover:text-red-500"
|
||||||
onClick={() => borrarLinea(linea.id)}
|
onClick={() => borrarLinea(linea.id)} // Aquí también podrías añadir una mutación delete
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user