@@ -26,8 +26,6 @@ import { qk } from '@/data/query/keys'
|
||||
export const Route = createFileRoute('/planes/$planId/_detalle')({
|
||||
loader: async ({ context: { queryClient }, params: { planId } }) => {
|
||||
try {
|
||||
console.log('loader')
|
||||
|
||||
await queryClient.ensureQueryData({
|
||||
queryKey: qk.plan(planId),
|
||||
queryFn: () => plans_get(planId),
|
||||
@@ -35,8 +33,6 @@ export const Route = createFileRoute('/planes/$planId/_detalle')({
|
||||
} catch (e: any) {
|
||||
// PGRST116: The result contains 0 rows
|
||||
if (e?.code === 'PGRST116') {
|
||||
console.log('not found on', Route.path)
|
||||
|
||||
throw notFound()
|
||||
}
|
||||
throw e
|
||||
@@ -85,7 +81,6 @@ function RouteComponent() {
|
||||
}
|
||||
|
||||
const handleSave = () => {
|
||||
console.log('Guardando en DB...', { nombrePlan, nivelPlan })
|
||||
// Aquí iría tu mutation
|
||||
setIsDirty(false)
|
||||
}
|
||||
@@ -221,11 +216,7 @@ function RouteComponent() {
|
||||
<Tab to="/planes/$planId/" params={{ planId }}>
|
||||
Datos Generales
|
||||
</Tab>
|
||||
<Tab
|
||||
to="/planes/$planId/mapa"
|
||||
params={{ planId }}
|
||||
search={{ ciclo: data?.numero_ciclos }}
|
||||
>
|
||||
<Tab to="/planes/$planId/mapa" params={{ planId }}>
|
||||
Mapa Curricular
|
||||
</Tab>
|
||||
<Tab to="/planes/$planId/asignaturas" params={{ planId }}>
|
||||
@@ -240,13 +231,7 @@ function RouteComponent() {
|
||||
<Tab to="/planes/$planId/documento" params={{ planId }}>
|
||||
Documento
|
||||
</Tab>
|
||||
<Tab
|
||||
to="/planes/$planId/historial"
|
||||
params={{ planId }}
|
||||
search={{
|
||||
structure: data?.estructuras_plan?.definicion?.properties,
|
||||
}}
|
||||
>
|
||||
<Tab to="/planes/$planId/historial" params={{ planId }}>
|
||||
Historial
|
||||
</Tab>
|
||||
</nav>
|
||||
@@ -300,7 +285,6 @@ const InfoCard = forwardRef<
|
||||
function Tab({
|
||||
to,
|
||||
params,
|
||||
search,
|
||||
children,
|
||||
}: {
|
||||
to: string
|
||||
@@ -308,12 +292,10 @@ function Tab({
|
||||
search?: any
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
console.log(search)
|
||||
return (
|
||||
<Link
|
||||
to={to}
|
||||
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"
|
||||
activeProps={{ className: 'border-teal-600 text-teal-700 font-bold' }}
|
||||
activeOptions={{
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
History,
|
||||
Calendar,
|
||||
} from 'lucide-react'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Card, CardContent } from '@/components/ui/card'
|
||||
@@ -23,13 +23,10 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} 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')({
|
||||
component: RouteComponent,
|
||||
validateSearch: (search: { structure?: any }) => ({
|
||||
structure: search.structure ?? null,
|
||||
}),
|
||||
})
|
||||
|
||||
const getEventConfig = (tipo: string, campo: string) => {
|
||||
@@ -61,14 +58,23 @@ const getEventConfig = (tipo: string, campo: string) => {
|
||||
function RouteComponent() {
|
||||
const { planId } = Route.useParams()
|
||||
const { data: rawData, isLoading } = usePlanHistorial(planId)
|
||||
const { structure } = Route.useSearch()
|
||||
console.log(structure?.vigencia?.title)
|
||||
console.log(structure)
|
||||
const [structure, setStructure] = useState<any>(null)
|
||||
const { data } = usePlan(planId)
|
||||
console.log('analizando estructura')
|
||||
|
||||
console.log(data?.estructuras_plan?.definicion?.properties)
|
||||
// console.log(structure)
|
||||
|
||||
// ESTADOS PARA EL MODAL
|
||||
const [selectedEvent, setSelectedEvent] = useState<any>(null)
|
||||
const [isModalOpen, setIsModalOpen] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (data?.estructuras_plan?.definicion?.properties) {
|
||||
setStructure(data.estructuras_plan.definicion.properties)
|
||||
}
|
||||
}, [data])
|
||||
|
||||
const historyEvents = useMemo(() => {
|
||||
if (!rawData) return []
|
||||
return rawData.map((item: any) => {
|
||||
|
||||
@@ -107,8 +107,6 @@ function DatosGeneralesPage() {
|
||||
|
||||
setCampos(datosTransformados)
|
||||
}
|
||||
|
||||
console.log(properties)
|
||||
}, [data])
|
||||
|
||||
// 3. Manejadores de acciones (Ahora como funciones locales)
|
||||
@@ -166,8 +164,6 @@ function DatosGeneralesPage() {
|
||||
}
|
||||
|
||||
const handleIARequest = (clave: string) => {
|
||||
console.log(clave)
|
||||
|
||||
navigate({
|
||||
to: '/planes/$planId/iaplan',
|
||||
params: {
|
||||
@@ -190,9 +186,8 @@ function DatosGeneralesPage() {
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-6 lg:grid-cols-2">
|
||||
{campos.map((campo, key) => {
|
||||
{campos.map((campo) => {
|
||||
const isEditing = editingId === campo.id
|
||||
console.log(campo)
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -37,6 +37,7 @@ import {
|
||||
import {
|
||||
useCreateLinea,
|
||||
useDeleteLinea,
|
||||
usePlan,
|
||||
usePlanAsignaturas,
|
||||
usePlanLineas,
|
||||
useUpdateAsignatura,
|
||||
@@ -166,44 +167,39 @@ function AsignaturaCardItem({
|
||||
|
||||
export const Route = createFileRoute('/planes/$planId/_detalle/mapa')({
|
||||
component: MapaCurricularPage,
|
||||
validateSearch: (search: { ciclo?: number }) => ({
|
||||
ciclo: search.ciclo ?? null,
|
||||
}),
|
||||
})
|
||||
|
||||
function MapaCurricularPage() {
|
||||
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 [tempNombreLinea, setTempNombreLinea] = useState('')
|
||||
const { mutate: createLinea } = useCreateLinea()
|
||||
const { mutate: updateLineaApi } = useUpdateLinea()
|
||||
const { mutate: deleteLineaApi } = useDeleteLinea()
|
||||
// 1. Fetch de Datos
|
||||
const { data: asignaturasApi, isLoading: loadingAsig } =
|
||||
usePlanAsignaturas(planId)
|
||||
const { data: lineasApi, isLoading: loadingLineas } = usePlanLineas(planId)
|
||||
|
||||
// 2. Estado Local (Para interactividad)
|
||||
const [asignaturas, setAsignaturas] = useState<Array<Asignatura>>([])
|
||||
const [lineas, setLineas] = useState<Array<LineaCurricular>>([])
|
||||
const [draggedAsignatura, setDraggedAsignatura] = useState<string | null>(
|
||||
null,
|
||||
)
|
||||
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 { 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 nombreNormalizado = nombre.trim()
|
||||
|
||||
// 1. Validar vacío
|
||||
if (!nombreNormalizado) return
|
||||
|
||||
// 2. Validar duplicados en el estado local (Insensible a mayúsculas/acentos)
|
||||
const nombreBusqueda = nombreNormalizado
|
||||
.toLowerCase()
|
||||
.normalize('NFD')
|
||||
@@ -219,12 +215,9 @@ function MapaCurricularPage() {
|
||||
|
||||
if (yaExiste) {
|
||||
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)
|
||||
|
||||
createLinea(
|
||||
{
|
||||
nombre: nombreNormalizado,
|
||||
@@ -234,7 +227,6 @@ function MapaCurricularPage() {
|
||||
},
|
||||
{
|
||||
onSuccess: (nueva) => {
|
||||
// Sincronización local que ya teníamos
|
||||
const mapeada = {
|
||||
id: nueva.id,
|
||||
nombre: nueva.nombre,
|
||||
@@ -280,7 +272,6 @@ function MapaCurricularPage() {
|
||||
)
|
||||
}, [lineas])
|
||||
|
||||
// 3. Sincronizar API -> Estado Local
|
||||
useEffect(() => {
|
||||
if (asignaturasApi)
|
||||
setAsignaturas(mapAsignaturasToAsignaturas(asignaturasApi))
|
||||
@@ -292,20 +283,27 @@ function MapaCurricularPage() {
|
||||
|
||||
const ciclosTotales = Number(ciclo)
|
||||
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 handleDecimalChange = (value: string, max?: number): string | null => {
|
||||
if (value === '') return ''
|
||||
|
||||
// 1. FUNCION DE GUARDAR MODAL
|
||||
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 = () => {
|
||||
if (!editingData) return
|
||||
console.log(asignaturas)
|
||||
|
||||
setAsignaturas((prev) =>
|
||||
prev.map((m) => (m.id === editingData.id ? { ...editingData } : m)),
|
||||
)
|
||||
// setIsEditModalOpen(false)
|
||||
// Preparamos el patch con la estructura de tu tabla
|
||||
const patch = {
|
||||
nombre: editingData.nombre,
|
||||
codigo: editingData.clave,
|
||||
@@ -332,22 +330,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(
|
||||
(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) => {
|
||||
// 1. Opcional: Confirmación de seguridad
|
||||
if (
|
||||
!confirm(
|
||||
'¿Estás seguro de eliminar esta línea? Las materias asignadas volverán a la bandeja de entrada.',
|
||||
@@ -356,11 +343,8 @@ function MapaCurricularPage() {
|
||||
return
|
||||
}
|
||||
|
||||
// 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) =>
|
||||
@@ -369,8 +353,6 @@ function MapaCurricularPage() {
|
||||
: asig,
|
||||
),
|
||||
)
|
||||
|
||||
// Segundo: Quitamos la línea del estado
|
||||
setLineas((prev) => prev.filter((l) => l.id !== id))
|
||||
},
|
||||
onError: (error) => {
|
||||
@@ -427,8 +409,6 @@ function MapaCurricularPage() {
|
||||
: m,
|
||||
),
|
||||
)
|
||||
|
||||
// 2. Persistir en la API
|
||||
const patch = {
|
||||
numero_ciclo: ciclo,
|
||||
linea_plan_id: lineaId,
|
||||
@@ -774,13 +754,17 @@ function MapaCurricularPage() {
|
||||
</label>
|
||||
<Input
|
||||
type="number"
|
||||
min={0}
|
||||
value={editingData.creditos}
|
||||
onChange={(e) =>
|
||||
setEditingData({
|
||||
...editingData,
|
||||
creditos: Number(e.target.value),
|
||||
})
|
||||
}
|
||||
onChange={(e) => {
|
||||
const val = handleDecimalChange(e.target.value, 10)
|
||||
if (val !== null) {
|
||||
setEditingData({
|
||||
...editingData,
|
||||
creditos: val === '' ? 0 : Number(val),
|
||||
})
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
@@ -790,12 +774,15 @@ function MapaCurricularPage() {
|
||||
<Input
|
||||
type="number"
|
||||
value={editingData.hd}
|
||||
onChange={(e) =>
|
||||
setEditingData({
|
||||
...editingData,
|
||||
hd: Number(e.target.value),
|
||||
})
|
||||
}
|
||||
onChange={(e) => {
|
||||
const val = handleDecimalChange(e.target.value, 10)
|
||||
if (val !== null) {
|
||||
setEditingData({
|
||||
...editingData,
|
||||
hd: Number(e.target.value),
|
||||
})
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
@@ -805,12 +792,15 @@ function MapaCurricularPage() {
|
||||
<Input
|
||||
type="number"
|
||||
value={editingData.hi}
|
||||
onChange={(e) =>
|
||||
setEditingData({
|
||||
...editingData,
|
||||
hi: Number(e.target.value),
|
||||
})
|
||||
}
|
||||
onChange={(e) => {
|
||||
const val = handleDecimalChange(e.target.value, 10)
|
||||
if (val !== null) {
|
||||
setEditingData({
|
||||
...editingData,
|
||||
hi: Number(e.target.value),
|
||||
})
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -882,23 +872,29 @@ function MapaCurricularPage() {
|
||||
Seriación (Prerrequisitos)
|
||||
</label>
|
||||
<Select
|
||||
value={seriacionValue}
|
||||
onValueChange={(val) => {
|
||||
// Evitamos duplicados en la lista de prerrequisitos local
|
||||
if (val === 'unassigned') {
|
||||
setSeriacionValue('unassigned')
|
||||
return
|
||||
}
|
||||
if (!editingData.prerrequisitos.includes(val)) {
|
||||
setEditingData({
|
||||
...editingData,
|
||||
prerrequisitos: [...editingData.prerrequisitos, val],
|
||||
})
|
||||
}
|
||||
setSeriacionValue('unassigned')
|
||||
}}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Seleccionar asignatura..." />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{/* FILTRO CLAVE:
|
||||
Solo mostramos materias cuyo ID sea diferente al de la materia que estamos editando
|
||||
*/}
|
||||
<SelectItem value="unassigned">
|
||||
-- Sin Seriación --
|
||||
</SelectItem>
|
||||
|
||||
{asignaturas
|
||||
.filter((m) => m.id !== editingData.id)
|
||||
.map((m) => (
|
||||
|
||||
Reference in New Issue
Block a user