Compare commits
9 Commits
d3267e5890
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 6c744b8c89 | |||
| 18ba592378 | |||
| 1ede5bca32 | |||
| 80cc0a9f2b | |||
| 7b437c56e0 | |||
| b37d36fe15 | |||
| 03cdd24c1d | |||
| a98982d228 | |||
| c710b85090 |
@@ -128,8 +128,6 @@ export default function AsignaturaDetailPage() {
|
|||||||
}
|
}
|
||||||
/* ---------- sincronizar API ---------- */
|
/* ---------- sincronizar API ---------- */
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log(requisitosFormateados)
|
|
||||||
|
|
||||||
if (asignaturaApi) setAsignatura(asignaturaApi)
|
if (asignaturaApi) setAsignatura(asignaturaApi)
|
||||||
}, [asignaturaApi, requisitosFormateados])
|
}, [asignaturaApi, requisitosFormateados])
|
||||||
|
|
||||||
@@ -182,7 +180,6 @@ function DatosGenerales({
|
|||||||
|
|
||||||
const criteriosEvaluacion: Array<CriterioEvaluacionRow> = useMemo(() => {
|
const criteriosEvaluacion: Array<CriterioEvaluacionRow> = useMemo(() => {
|
||||||
const raw = (data as any)?.criterios_de_evaluacion
|
const raw = (data as any)?.criterios_de_evaluacion
|
||||||
console.log(raw)
|
|
||||||
|
|
||||||
if (!Array.isArray(raw)) return []
|
if (!Array.isArray(raw)) return []
|
||||||
|
|
||||||
@@ -232,7 +229,7 @@ function DatosGenerales({
|
|||||||
if (isLoading) return <p>Cargando información...</p>
|
if (isLoading) return <p>Cargando información...</p>
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="animate-in fade-in space-y-8 pb-8 duration-500">
|
<div className="animate-in fade-in mx-auto max-w-7xl space-y-8 px-4 py-8 duration-500">
|
||||||
{/* Encabezado de la Sección */}
|
{/* Encabezado de la Sección */}
|
||||||
<div className="flex flex-col justify-between gap-4 border-b pb-6 md:flex-row md:items-center">
|
<div className="flex flex-col justify-between gap-4 border-b pb-6 md:flex-row md:items-center">
|
||||||
<div>
|
<div>
|
||||||
@@ -414,8 +411,7 @@ function InfoCard({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setData(initialContent)
|
setData(initialContent)
|
||||||
setTempText(initialContent)
|
setTempText(initialContent)
|
||||||
console.log(data)
|
|
||||||
console.log(initialContent)
|
|
||||||
if (type === 'evaluation') {
|
if (type === 'evaluation') {
|
||||||
const raw = Array.isArray(initialContent) ? initialContent : []
|
const raw = Array.isArray(initialContent) ? initialContent : []
|
||||||
const rows: Array<CriterioEvaluacionRowDraft> = raw
|
const rows: Array<CriterioEvaluacionRowDraft> = raw
|
||||||
@@ -457,10 +453,6 @@ function InfoCard({
|
|||||||
}, [highlightToken])
|
}, [highlightToken])
|
||||||
|
|
||||||
const handleSave = () => {
|
const handleSave = () => {
|
||||||
console.log('clave, valor:', clave, String(tempText ?? ''))
|
|
||||||
console.log(clave)
|
|
||||||
console.log(tempText)
|
|
||||||
|
|
||||||
if (type === 'evaluation') {
|
if (type === 'evaluation') {
|
||||||
const cleaned: Array<CriterioEvaluacionRow> = []
|
const cleaned: Array<CriterioEvaluacionRow> = []
|
||||||
for (const r of evalRows) {
|
for (const r of evalRows) {
|
||||||
@@ -491,8 +483,6 @@ function InfoCard({
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (type === 'requirements') {
|
if (type === 'requirements') {
|
||||||
console.log('entre aqui ')
|
|
||||||
|
|
||||||
// Si tempText es un array y tiene elementos, tomamos el ID del primero
|
// Si tempText es un array y tiene elementos, tomamos el ID del primero
|
||||||
// Si es "none" o está vacío, mandamos null (para limpiar la seriación)
|
// Si es "none" o está vacío, mandamos null (para limpiar la seriación)
|
||||||
const prerequisiteId =
|
const prerequisiteId =
|
||||||
@@ -519,8 +509,6 @@ function InfoCard({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleIARequest = (campoClave: string) => {
|
const handleIARequest = (campoClave: string) => {
|
||||||
console.log(campoClave)
|
|
||||||
|
|
||||||
let targetClave = campoClave
|
let targetClave = campoClave
|
||||||
if (type === 'evaluation' && !targetClave) {
|
if (type === 'evaluation' && !targetClave) {
|
||||||
targetClave = 'criterios_de_evaluacion'
|
targetClave = 'criterios_de_evaluacion'
|
||||||
@@ -529,8 +517,6 @@ function InfoCard({
|
|||||||
if (targetClave === 'contenido') {
|
if (targetClave === 'contenido') {
|
||||||
targetClave = 'contenido_tematico'
|
targetClave = 'contenido_tematico'
|
||||||
}
|
}
|
||||||
console.log(targetClave)
|
|
||||||
console.log(asignaturaId)
|
|
||||||
|
|
||||||
navigate({
|
navigate({
|
||||||
to: '/planes/$planId/asignaturas/$asignaturaId/iaasignatura',
|
to: '/planes/$planId/asignaturas/$asignaturaId/iaasignatura',
|
||||||
@@ -652,18 +638,12 @@ function InfoCard({
|
|||||||
: 'none'
|
: 'none'
|
||||||
}
|
}
|
||||||
onValueChange={(val) => {
|
onValueChange={(val) => {
|
||||||
console.log(availableSubjects)
|
|
||||||
|
|
||||||
const selected = availableSubjects?.find(
|
const selected = availableSubjects?.find(
|
||||||
(s) => s.id === val,
|
(s) => s.id === val,
|
||||||
)
|
)
|
||||||
if (val === 'none' || !selected) {
|
if (val === 'none' || !selected) {
|
||||||
console.log('guardando')
|
|
||||||
|
|
||||||
setTempText([])
|
setTempText([])
|
||||||
} else {
|
} else {
|
||||||
console.log('hola')
|
|
||||||
|
|
||||||
setTempText([
|
setTempText([
|
||||||
{
|
{
|
||||||
id: selected.id,
|
id: selected.id,
|
||||||
@@ -677,20 +657,29 @@ function InfoCard({
|
|||||||
>
|
>
|
||||||
<SelectTrigger className="w-full">
|
<SelectTrigger className="w-full">
|
||||||
<div className="flex-1 truncate text-left">
|
<div className="flex-1 truncate text-left">
|
||||||
<SelectValue placeholder="Selecciona una materia" />
|
<SelectValue placeholder="Selecciona una materia">
|
||||||
|
{Array.isArray(tempText) && tempText.length > 0
|
||||||
|
? `${tempText[0].code} - ${tempText[0].name}`
|
||||||
|
: undefined}
|
||||||
|
</SelectValue>
|
||||||
</div>
|
</div>
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
|
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="none">
|
<SelectItem value="none">
|
||||||
Ninguna (Sin seriación)
|
Ninguna (Sin seriación)
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
|
|
||||||
{availableSubjects?.map((asig) => (
|
{availableSubjects?.map((asig) => (
|
||||||
<SelectItem
|
<SelectItem
|
||||||
key={asig.id}
|
key={asig.id}
|
||||||
value={asig.id}
|
value={asig.id}
|
||||||
className="max-w-[300px] sm:max-w-[500px]"
|
className="max-w-[300px] sm:max-w-[500px]"
|
||||||
>
|
>
|
||||||
<span className="block truncate">
|
<span className="text-primary font-bold">
|
||||||
|
[C{asig.numero_ciclo}]
|
||||||
|
</span>{' '}
|
||||||
|
<span className="inline-block truncate">
|
||||||
{asig.codigo} - {asig.nombre}
|
{asig.codigo} - {asig.nombre}
|
||||||
</span>
|
</span>
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import {
|
|||||||
Filter,
|
Filter,
|
||||||
Calendar,
|
Calendar,
|
||||||
Loader2,
|
Loader2,
|
||||||
Eye,
|
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { useState, useMemo } from 'react'
|
import { useState, useMemo } from 'react'
|
||||||
|
|
||||||
@@ -249,7 +248,16 @@ export function HistorialTab() {
|
|||||||
`bg-current ${config.color}`,
|
`bg-current ${config.color}`,
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Card className="card-interactive">
|
<Card
|
||||||
|
className="border-border card-interactive hover:border-primary/50 flex-1 cursor-pointer shadow-none transition-colors"
|
||||||
|
onClick={() => openCompareModal(cambio)}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter' || e.key === ' ')
|
||||||
|
openCompareModal(cambio)
|
||||||
|
}}
|
||||||
|
>
|
||||||
<CardContent className="py-4">
|
<CardContent className="py-4">
|
||||||
<div className="flex items-start gap-4">
|
<div className="flex items-start gap-4">
|
||||||
<div
|
<div
|
||||||
@@ -265,16 +273,7 @@ export function HistorialTab() {
|
|||||||
<p className="font-medium">
|
<p className="font-medium">
|
||||||
{cambio.descripcion}
|
{cambio.descripcion}
|
||||||
</p>
|
</p>
|
||||||
{/* BOTÓN PARA VER CAMBIOS */}
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
className="gap-2 text-blue-600 hover:bg-blue-50 hover:text-blue-700"
|
|
||||||
onClick={() => openCompareModal(cambio)}
|
|
||||||
>
|
|
||||||
<Eye className="h-4 w-4" />
|
|
||||||
Ver cambios
|
|
||||||
</Button>
|
|
||||||
<span className="text-muted-foreground text-xs">
|
<span className="text-muted-foreground text-xs">
|
||||||
{format(cambio.fecha, 'HH:mm')}
|
{format(cambio.fecha, 'HH:mm')}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -36,7 +36,8 @@ export const AlertaConflicto = ({
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-2 text-xs font-semibold text-slate-500">
|
<p className="mt-2 text-xs font-semibold text-slate-500">
|
||||||
¿Deseas ignorar la regla y moverla de todos modos?
|
¿Deseas ignorar la regla y moverla de todos modos (Esto eliminará la
|
||||||
|
seriación)?
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -62,13 +63,8 @@ export const AlertaConflicto = ({
|
|||||||
|
|
||||||
<div className="mt-8 flex flex-col-reverse justify-end gap-3 sm:flex-row">
|
<div className="mt-8 flex flex-col-reverse justify-end gap-3 sm:flex-row">
|
||||||
<AlertDialog.Cancel asChild>
|
<AlertDialog.Cancel asChild>
|
||||||
<Button
|
<Button variant="ghost">Cancelar</Button>
|
||||||
variant="ghost"
|
{/* Radix automáticamente llamará a onOpenChange(false) al hacer clic aquí */}
|
||||||
onClick={() => onConfirm()} // Si tu componente espera resolve
|
|
||||||
className="font-semibold text-slate-500 hover:text-slate-700"
|
|
||||||
>
|
|
||||||
Cancelar
|
|
||||||
</Button>
|
|
||||||
</AlertDialog.Cancel>
|
</AlertDialog.Cancel>
|
||||||
|
|
||||||
<AlertDialog.Action asChild>
|
<AlertDialog.Action asChild>
|
||||||
|
|||||||
@@ -9,11 +9,13 @@ export function LoginCard() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full max-w-md rounded-2xl bg-white p-8 shadow-xl">
|
<div className="w-full max-w-md rounded-2xl bg-white p-8 shadow-xl">
|
||||||
<img
|
<div className="flex justify-center">
|
||||||
src="/4_IMAGOTIPO_LASALLE_MEXICO_COLOR_RGB_2020.png"
|
<img
|
||||||
alt="La Salle México"
|
src="/4_IMAGOTIPO_LASALLE_MEXICO_COLOR_RGB_2020.png"
|
||||||
className="mb-6 h-20 w-auto"
|
alt="La Salle México"
|
||||||
/>
|
className="mb-6 h-20 w-auto"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<h1 className="mb-1 text-center text-2xl font-semibold">
|
<h1 className="mb-1 text-center text-2xl font-semibold">
|
||||||
Iniciar sesión
|
Iniciar sesión
|
||||||
</h1>
|
</h1>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import type { VariantProps } from 'class-variance-authority'
|
|||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
const buttonVariants = cva(
|
const buttonVariants = cva(
|
||||||
"focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
"focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:ring-[3px] disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import {
|
|||||||
User,
|
User,
|
||||||
Loader2,
|
Loader2,
|
||||||
Clock,
|
Clock,
|
||||||
Eye,
|
|
||||||
History,
|
History,
|
||||||
Calendar,
|
Calendar,
|
||||||
ChevronLeft,
|
ChevronLeft,
|
||||||
@@ -156,7 +155,16 @@ function RouteComponent() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Card className="border-border hover:border-primary/50 flex-1 shadow-none transition-colors">
|
<Card
|
||||||
|
className="border-border hover:border-primary/50 flex-1 cursor-pointer shadow-none transition-colors"
|
||||||
|
onClick={() => openCompareModal(event)}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter' || e.key === ' ')
|
||||||
|
openCompareModal(event)
|
||||||
|
}}
|
||||||
|
>
|
||||||
<CardContent className="p-4">
|
<CardContent className="p-4">
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
{/* LÍNEA SUPERIOR: Título a la izquierda --- Usuario, Botón y Fecha a la derecha */}
|
{/* LÍNEA SUPERIOR: Título a la izquierda --- Usuario, Botón y Fecha a la derecha */}
|
||||||
@@ -186,15 +194,6 @@ function RouteComponent() {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Botón Ver Cambios */}
|
|
||||||
<button
|
|
||||||
onClick={() => openCompareModal(event)}
|
|
||||||
className="text-primary md:text-muted-foreground md:hover:text-primary group/btn flex items-center gap-1.5 text-xs font-medium"
|
|
||||||
>
|
|
||||||
<Eye className="text-muted-foreground/70 group-hover/btn:text-primary h-4 w-4" />
|
|
||||||
<span>Ver cambios</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* Fecha exacta (Solo visible en desktop para no amontonar) */}
|
{/* Fecha exacta (Solo visible en desktop para no amontonar) */}
|
||||||
<span className="text-muted-foreground/70 hidden text-[11px] lg:block">
|
<span className="text-muted-foreground/70 hidden text-[11px] lg:block">
|
||||||
{format(event.date, 'yyyy-MM-dd HH:mm')}
|
{format(event.date, 'yyyy-MM-dd HH:mm')}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { useMemo, useState, useEffect, Fragment, useRef } from 'react'
|
|||||||
|
|
||||||
import type { TipoAsignatura } from '@/data'
|
import type { TipoAsignatura } from '@/data'
|
||||||
import type { Asignatura } from '@/types/plan'
|
import type { Asignatura } from '@/types/plan'
|
||||||
import type { TablesUpdate } from '@/types/supabase'
|
|
||||||
|
|
||||||
import { AlertaConflicto } from '@/components/asignaturas/detalle/mapa/AlertaConflicto'
|
import { AlertaConflicto } from '@/components/asignaturas/detalle/mapa/AlertaConflicto'
|
||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
@@ -402,15 +401,19 @@ function MapaCurricularPage() {
|
|||||||
asignatura: Asignatura,
|
asignatura: Asignatura,
|
||||||
nuevoCiclo: number,
|
nuevoCiclo: number,
|
||||||
) => {
|
) => {
|
||||||
|
// 1. Esperamos la interacción del usuario
|
||||||
const acepto = await validarConInterrupcion(asignatura.id, nuevoCiclo)
|
const acepto = await validarConInterrupcion(asignatura.id, nuevoCiclo)
|
||||||
|
|
||||||
if (!acepto) {
|
// 2. Limpiamos el estado del modal ANTES de seguir
|
||||||
setConfirmState(null)
|
setConfirmState(null)
|
||||||
return
|
|
||||||
|
// 3. Verificamos la respuesta
|
||||||
|
if (acepto === false) {
|
||||||
|
return // Detenemos la ejecución aquí
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 4. Solo si acepto es true, ejecutamos la API
|
||||||
const patch = { numero_ciclo: nuevoCiclo }
|
const patch = { numero_ciclo: nuevoCiclo }
|
||||||
|
|
||||||
updateAsignatura(
|
updateAsignatura(
|
||||||
{ asignaturaId: asignatura.id, patch: patch as any },
|
{ asignaturaId: asignatura.id, patch: patch as any },
|
||||||
{
|
{
|
||||||
@@ -420,12 +423,9 @@ function MapaCurricularPage() {
|
|||||||
m.id === asignatura.id ? { ...m, ciclo: nuevoCiclo } : m,
|
m.id === asignatura.id ? { ...m, ciclo: nuevoCiclo } : m,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
setConfirmState(null) // Cerramos el modal de Radix al tener éxito
|
|
||||||
},
|
},
|
||||||
onError: (err) => {
|
onError: (err) => {
|
||||||
// En lugar de alert nativo, podrías usar un toast aquí
|
|
||||||
console.error('Error al actualizar:', err)
|
console.error('Error al actualizar:', err)
|
||||||
setConfirmState(null)
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -587,48 +587,61 @@ function MapaCurricularPage() {
|
|||||||
|
|
||||||
return val
|
return val
|
||||||
}
|
}
|
||||||
const handleSaveChanges = () => {
|
|
||||||
if (!editingData) return
|
const procesarCambioAsignatura = async (
|
||||||
setAsignaturas((prev) =>
|
asignaturaId: string,
|
||||||
prev.map((m) => (m.id === editingData.id ? { ...editingData } : m)),
|
nuevosDatos: Partial<Asignatura>,
|
||||||
)
|
) => {
|
||||||
type AsignaturaPatch = {
|
const asignaturaOriginal = asignaturas.find((a) => a.id === asignaturaId)
|
||||||
codigo?: TablesUpdate<'asignaturas'>['codigo']
|
if (!asignaturaOriginal) return
|
||||||
nombre?: TablesUpdate<'asignaturas'>['nombre']
|
|
||||||
tipo?: TablesUpdate<'asignaturas'>['tipo']
|
// ¿Cambió el ciclo? Si es así, validamos seriación
|
||||||
creditos?: TablesUpdate<'asignaturas'>['creditos']
|
if (
|
||||||
horas_academicas?: TablesUpdate<'asignaturas'>['horas_academicas']
|
nuevosDatos.ciclo !== undefined &&
|
||||||
horas_independientes?: TablesUpdate<'asignaturas'>['horas_independientes']
|
nuevosDatos.ciclo !== asignaturaOriginal.ciclo
|
||||||
numero_ciclo?: TablesUpdate<'asignaturas'>['numero_ciclo']
|
) {
|
||||||
linea_plan_id?: TablesUpdate<'asignaturas'>['linea_plan_id']
|
const acepto = await validarConInterrupcion(
|
||||||
prerrequisito_asignatura_id?: string | null
|
asignaturaId,
|
||||||
|
nuevosDatos.ciclo,
|
||||||
|
)
|
||||||
|
setConfirmState(null)
|
||||||
|
if (!acepto) return // El usuario canceló, no guardamos nada
|
||||||
}
|
}
|
||||||
const patch: Partial<AsignaturaPatch> = {
|
|
||||||
nombre: editingData.nombre,
|
// Si llegamos aquí, o no cambió el ciclo o el usuario aceptó el conflicto
|
||||||
codigo: editingData.clave,
|
const patch = {
|
||||||
creditos: editingData.creditos,
|
nombre: nuevosDatos.nombre ?? asignaturaOriginal.nombre,
|
||||||
horas_academicas: editingData.hd,
|
codigo: nuevosDatos.clave ?? asignaturaOriginal.clave,
|
||||||
horas_independientes: editingData.hi,
|
numero_ciclo: nuevosDatos.ciclo,
|
||||||
numero_ciclo: editingData.ciclo,
|
linea_plan_id: nuevosDatos.lineaCurricularId,
|
||||||
linea_plan_id: editingData.lineaCurricularId,
|
creditos: nuevosDatos.creditos,
|
||||||
prerrequisito_asignatura_id: editingData.prerrequisito_asignatura_id,
|
horas_academicas: nuevosDatos.hd,
|
||||||
tipo: editingData.tipo.toUpperCase() as TipoAsignatura, // Asegurar que coincida con el ENUM (OBLIGATORIA/OPTATIVA)
|
horas_independientes: nuevosDatos.hi,
|
||||||
|
prerrequisito_asignatura_id: nuevosDatos.prerrequisito_asignatura_id,
|
||||||
|
tipo: nuevosDatos.tipo?.toUpperCase() as TipoAsignatura,
|
||||||
}
|
}
|
||||||
|
|
||||||
updateAsignatura(
|
updateAsignatura(
|
||||||
{ asignaturaId: editingData.id, patch: patch as any },
|
{ asignaturaId, patch: patch as any },
|
||||||
{
|
{
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
setIsEditModalOpen(false)
|
setAsignaturas((prev) =>
|
||||||
// Opcional: Mostrar un toast de éxito
|
prev.map((m) =>
|
||||||
},
|
m.id === asignaturaId ? { ...m, ...nuevosDatos } : m,
|
||||||
onError: (error) => {
|
),
|
||||||
console.error('Error al guardar:', error)
|
)
|
||||||
alert('Hubo un error al guardar los cambios.')
|
setIsEditModalOpen(false) // Cerramos el modal si estaba abierto
|
||||||
},
|
},
|
||||||
|
onError: (err) => console.error('Error al guardar:', err),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
const handleSaveChanges = () => {
|
||||||
|
if (!editingData) return
|
||||||
|
|
||||||
|
// Llamamos a la lógica centralizada que incluye la alerta
|
||||||
|
procesarCambioAsignatura(editingData.id, editingData)
|
||||||
|
}
|
||||||
const unassignedAsignaturas = asignaturas.filter(
|
const unassignedAsignaturas = asignaturas.filter(
|
||||||
(m) => m.ciclo === null || m.lineaCurricularId === null,
|
(m) => m.ciclo === null || m.lineaCurricularId === null,
|
||||||
)
|
)
|
||||||
@@ -700,41 +713,13 @@ function MapaCurricularPage() {
|
|||||||
lineaId: string | null,
|
lineaId: string | null,
|
||||||
) => {
|
) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
||||||
if (!draggedAsignatura) return
|
if (!draggedAsignatura) return
|
||||||
|
|
||||||
// Buscamos la asignatura completa para tener sus datos
|
// Solo disparamos la lógica si realmente hay un cambio de posición
|
||||||
const asignatura = asignaturas.find((a) => a.id === draggedAsignatura)
|
procesarCambioAsignatura(draggedAsignatura, {
|
||||||
if (!asignatura) return
|
ciclo: cicloDestino,
|
||||||
|
lineaCurricularId: lineaId,
|
||||||
// SI EL CAMBIO ES DE CICLO (y no solo de línea o a la bandeja)
|
})
|
||||||
// Ejecutamos la validación segura
|
|
||||||
if (cicloDestino !== null && cicloDestino !== asignatura.ciclo) {
|
|
||||||
// Llamamos a la función que agregaste
|
|
||||||
// IMPORTANTE: Asegúrate que handleCambioCicloSeguro esté definida ANTES o sea accesible
|
|
||||||
await handleCambioCicloSeguro(asignatura, cicloDestino)
|
|
||||||
|
|
||||||
// Si la validación interna de handleCambioCicloSeguro falla o el usuario cancela,
|
|
||||||
// la ejecución se detiene dentro de esa función.
|
|
||||||
} else {
|
|
||||||
// CASO B: Es un movimiento a la bandeja (null) o cambio de línea en el mismo ciclo
|
|
||||||
// Mantenemos la lógica simple de actualización
|
|
||||||
setAsignaturas((prev) =>
|
|
||||||
prev.map((m) =>
|
|
||||||
m.id === draggedAsignatura
|
|
||||||
? { ...m, ciclo: cicloDestino, lineaCurricularId: lineaId }
|
|
||||||
: m,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
updateAsignatura(
|
|
||||||
{
|
|
||||||
asignaturaId: draggedAsignatura,
|
|
||||||
patch: { numero_ciclo: cicloDestino, linea_plan_id: lineaId },
|
|
||||||
},
|
|
||||||
{ onError: (err) => console.error('Error al mover:', err) },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
setDraggedAsignatura(null)
|
setDraggedAsignatura(null)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,9 +17,10 @@ import {
|
|||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
|
|
||||||
|
import { AlertaConflicto } from '@/components/asignaturas/detalle/mapa/AlertaConflicto'
|
||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
import { lateralConfetti } from '@/components/ui/lateral-confetti'
|
import { lateralConfetti } from '@/components/ui/lateral-confetti'
|
||||||
import { useSubject, useUpdateAsignatura } from '@/data'
|
import { useSubject, useUpdateAsignatura, usePlanAsignaturas } from '@/data'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
export const Route = createFileRoute(
|
export const Route = createFileRoute(
|
||||||
@@ -167,6 +168,45 @@ function AsignaturaLayout() {
|
|||||||
|
|
||||||
const { data: asignaturaApi, isLoading: loadingAsig } =
|
const { data: asignaturaApi, isLoading: loadingAsig } =
|
||||||
useSubject(asignaturaId)
|
useSubject(asignaturaId)
|
||||||
|
const { data: todasLasAsignaturas } = usePlanAsignaturas(planId)
|
||||||
|
const [confirmState, setConfirmState] = useState<{
|
||||||
|
isOpen: boolean
|
||||||
|
resolve: (value: boolean) => void
|
||||||
|
mensaje: string
|
||||||
|
} | null>(null)
|
||||||
|
const validarConInterrupcion = async (
|
||||||
|
nuevoCiclo: number,
|
||||||
|
): Promise<boolean> => {
|
||||||
|
if (!todasLasAsignaturas || !asignaturaApi) return true
|
||||||
|
|
||||||
|
const materiasConflicto = todasLasAsignaturas.filter((a) => {
|
||||||
|
const esPrerrequisitoConflictivo =
|
||||||
|
asignaturaApi.prerrequisito_asignatura_id === a.id &&
|
||||||
|
(a.numero_ciclo ?? 0) >= nuevoCiclo
|
||||||
|
|
||||||
|
const esDependienteConflictiva =
|
||||||
|
a.prerrequisito_asignatura_id === asignaturaApi.id &&
|
||||||
|
(a.numero_ciclo ?? 0) <= nuevoCiclo
|
||||||
|
|
||||||
|
return esPrerrequisitoConflictivo || esDependienteConflictiva
|
||||||
|
})
|
||||||
|
|
||||||
|
if (materiasConflicto.length === 0) return true
|
||||||
|
|
||||||
|
const listaNombres = materiasConflicto.map((m) => m.nombre)
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
setConfirmState({
|
||||||
|
isOpen: true,
|
||||||
|
resolve,
|
||||||
|
mensaje: JSON.stringify({
|
||||||
|
main: `Mover "${asignaturaApi.nombre}" al ciclo ${nuevoCiclo} genera conflictos con:`,
|
||||||
|
materias: listaNombres,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const updateAsignatura = useUpdateAsignatura()
|
const updateAsignatura = useUpdateAsignatura()
|
||||||
|
|
||||||
const [headerData, setHeaderData] = useState({
|
const [headerData, setHeaderData] = useState({
|
||||||
@@ -187,7 +227,25 @@ function AsignaturaLayout() {
|
|||||||
}
|
}
|
||||||
}, [asignaturaApi])
|
}, [asignaturaApi])
|
||||||
|
|
||||||
const handleUpdateHeader = (key: string, value: string | number) => {
|
const handleUpdateHeader = async (key: string, value: string | number) => {
|
||||||
|
// 1. Si es ciclo, validamos antes de hacer nada
|
||||||
|
if (key === 'ciclo') {
|
||||||
|
const nuevoCiclo = Number(value)
|
||||||
|
const acepto = await validarConInterrupcion(nuevoCiclo)
|
||||||
|
|
||||||
|
setConfirmState(null) // Cerramos el modal tras la respuesta
|
||||||
|
|
||||||
|
if (!acepto) {
|
||||||
|
// Revertimos el estado local al valor de la API si cancela
|
||||||
|
setHeaderData((prev) => ({
|
||||||
|
...prev,
|
||||||
|
ciclo: asignaturaApi?.numero_ciclo ?? 0,
|
||||||
|
}))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Si no es ciclo o si aceptó el conflicto, procedemos
|
||||||
const newData = { ...headerData, [key]: value }
|
const newData = { ...headerData, [key]: value }
|
||||||
setHeaderData(newData)
|
setHeaderData(newData)
|
||||||
|
|
||||||
@@ -246,7 +304,7 @@ function AsignaturaLayout() {
|
|||||||
{/* Badge Estático del Tipo (Estilo oscuro sutil) */}
|
{/* Badge Estático del Tipo (Estilo oscuro sutil) */}
|
||||||
<Badge
|
<Badge
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="flex h-8 items-center gap-1.5 border-white/10 bg-white/5 px-3 text-white hover:border-white/20 hover:bg-white/10"
|
className="flex h-8 cursor-default items-center gap-1.5 border-white/10 bg-white/5 px-3 text-white"
|
||||||
>
|
>
|
||||||
<Tag size={12} className="text-white/70" />
|
<Tag size={12} className="text-white/70" />
|
||||||
{asignaturaApi.tipo}
|
{asignaturaApi.tipo}
|
||||||
@@ -294,10 +352,36 @@ function AsignaturaLayout() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* TABS NAVEGACIÓN (Se mantiene semántico para el cuerpo de la página) */}
|
{/* TABS NAVEGACIÓN (Se mantiene semántico para el cuerpo de la página)
|
||||||
<nav className="border-border bg-background/80 sticky top-0 z-20 border-b backdrop-blur-md">
|
<nav className="border-border bg-background/80 sticky top-0 z-20 border-b backdrop-blur-md">
|
||||||
<div className="mx-auto px-4 py-1 md:px-6 lg:px-8">
|
<div className="mx-auto px-4 py-1 md:px-6 lg:px-8">
|
||||||
<div className="scrollbar-hide flex items-center justify-start gap-8 overflow-x-auto whitespace-nowrap md:justify-start">
|
<div className="scrollbar-hide flex items-center justify-start gap-8 overflow-x-auto whitespace-nowrap md:justify-start">
|
||||||
|
*/}
|
||||||
|
{confirmState && (
|
||||||
|
<AlertaConflicto
|
||||||
|
isOpen={confirmState.isOpen}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (!open) {
|
||||||
|
confirmState.resolve(false)
|
||||||
|
setConfirmState(null)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onConfirm={() => confirmState.resolve(true)}
|
||||||
|
titulo="Conflicto de Seriación"
|
||||||
|
descripcion={confirmState.mensaje}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* TABS */}
|
||||||
|
|
||||||
|
<nav className="sticky top-0 z-20 border-b bg-white">
|
||||||
|
<div className="mx-auto p-4 py-2 md:px-6 lg:px-8">
|
||||||
|
{/* CAMBIOS CLAVE:
|
||||||
|
1. overflow-x-auto: Permite scroll horizontal.
|
||||||
|
2. scrollbar-hide: (Opcional) para que no se vea la barra fea.
|
||||||
|
3. justify-start md:justify-center: Alineado a la izquierda en móvil para que el scroll funcione, centrado en desktop.
|
||||||
|
*/}
|
||||||
|
<div className="no-scrollbar flex items-center justify-start gap-8 overflow-x-auto whitespace-nowrap md:justify-start">
|
||||||
{[
|
{[
|
||||||
{ label: 'Datos', to: '' },
|
{ label: 'Datos', to: '' },
|
||||||
{ label: 'Contenido', to: 'contenido' },
|
{ label: 'Contenido', to: 'contenido' },
|
||||||
|
|||||||
@@ -194,9 +194,7 @@ function RouteComponent() {
|
|||||||
variant="secondary"
|
variant="secondary"
|
||||||
onClick={resetFilters}
|
onClick={resetFilters}
|
||||||
disabled={isClearDisabled}
|
disabled={isClearDisabled}
|
||||||
className={`shadow-md ${
|
className={`shadow-md`}
|
||||||
isClearDisabled ? 'cursor-not-allowed opacity-50' : ''
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<Icons.X className="h-4 w-4" /> Limpiar
|
<Icons.X className="h-4 w-4" /> Limpiar
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -346,3 +346,15 @@ body {
|
|||||||
.login-bg {
|
.login-bg {
|
||||||
background-image: url('/fondo_login.png');
|
background-image: url('/fondo_login.png');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
button:not(:disabled),
|
||||||
|
[role='button']:not(:disabled) {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:disabled,
|
||||||
|
[role='button']:disabled {
|
||||||
|
cursor: not-allowed !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user