Refactor: unifica wizards con WizardLayout/WizardResponsiveHeader y convierte asignaturas en layout con Outlet

- Se introdujo un layout genérico de wizard (WizardLayout) con headerSlot/footerSlot y se migraron los modales de Nuevo Plan y Nueva Asignatura a esta estructura usando defineStepper.
- Se creó y reutilizó WizardResponsiveHeader para un encabezado responsivo consistente (progreso en móvil y navegación en escritorio) en ambos wizards.
- Se homologó WizardControls del wizard de asignaturas para alinearlo al patrón del wizard de planes (props onPrev/onNext, flags de disable, manejo de error/loading y creación).
- Se mejoró la captura de datos en el wizard de asignatura: créditos como flotante con 2 decimales, placeholders/estilos en inputs/selects y uso de catálogo real de estructuras vía useSubjectEstructuras con qk.estructurasAsignatura.
- Se reorganizó la sección de asignaturas del detalle del plan: el contenido del antiguo index se movió a asignaturas.tsx como layout y se agregó <Outlet />; navegación a “nueva asignatura” ajustada al path correcto.
This commit is contained in:
2026-02-04 13:36:46 -06:00
parent 1acb18711f
commit f3414f23f6
17 changed files with 834 additions and 441 deletions

View File

@@ -1,19 +1,20 @@
import { useState } from "react";
import { useState } from 'react'
import type { AsignaturaPreview, NewSubjectWizardState } from "../types";
import type { AsignaturaPreview, NewSubjectWizardState } from '../types'
export function useNuevaAsignaturaWizard(planId: string) {
const [wizard, setWizard] = useState<NewSubjectWizardState>({
step: 1,
planId,
modoCreacion: null,
plan_estudio_id: planId,
tipoOrigen: null,
datosBasicos: {
nombre: "",
clave: "",
tipo: "OBLIGATORIA",
creditos: 0,
horasSemana: 0,
estructuraId: "",
nombre: '',
codigo: '',
tipo: null,
creditos: null,
horasAcademicas: null,
horasIndependientes: null,
estructuraId: '',
},
clonInterno: {},
clonTradicional: {
@@ -21,42 +22,47 @@ export function useNuevaAsignaturaWizard(planId: string) {
archivosAdicionalesIds: [],
},
iaConfig: {
descripcionEnfoque: "",
notasAdicionales: "",
archivosExistentesIds: [],
descripcionEnfoqueAcademico: '',
instruccionesAdicionalesIA: '',
archivosReferencia: [],
repositoriosReferencia: [],
archivosAdjuntos: [],
},
resumen: {},
isLoading: false,
errorMessage: null,
});
})
const canContinueDesdeMetodo = wizard.modoCreacion === "MANUAL" ||
wizard.modoCreacion === "IA" ||
(wizard.modoCreacion === "CLONADO" && !!wizard.subModoClonado);
const canContinueDesdeMetodo =
wizard.tipoOrigen === 'MANUAL' ||
wizard.tipoOrigen === 'IA' ||
wizard.tipoOrigen === 'CLONADO_INTERNO' ||
wizard.tipoOrigen === 'CLONADO_TRADICIONAL'
const canContinueDesdeBasicos = !!wizard.datosBasicos.nombre &&
const canContinueDesdeBasicos =
!!wizard.datosBasicos.nombre &&
wizard.datosBasicos.tipo !== null &&
wizard.datosBasicos.creditos !== null &&
wizard.datosBasicos.creditos > 0 &&
!!wizard.datosBasicos.estructuraId;
!!wizard.datosBasicos.estructuraId
const canContinueDesdeConfig = (() => {
if (wizard.modoCreacion === "MANUAL") return true;
if (wizard.modoCreacion === "IA") {
return !!wizard.iaConfig?.descripcionEnfoque;
const canContinueDesdeDetalles = (() => {
if (wizard.tipoOrigen === 'MANUAL') return true
if (wizard.tipoOrigen === 'IA') {
return !!wizard.iaConfig?.descripcionEnfoqueAcademico
}
if (wizard.modoCreacion === "CLONADO") {
if (wizard.subModoClonado === "INTERNO") {
return !!wizard.clonInterno?.asignaturaOrigenId;
}
if (wizard.subModoClonado === "TRADICIONAL") {
return !!wizard.clonTradicional?.archivoWordAsignaturaId;
}
if (wizard.tipoOrigen === 'CLONADO_INTERNO') {
return !!wizard.clonInterno?.asignaturaOrigenId
}
return false;
})();
if (wizard.tipoOrigen === 'CLONADO_TRADICIONAL') {
return !!wizard.clonTradicional?.archivoWordAsignaturaId
}
return false
})()
const simularGeneracionIA = async () => {
setWizard((w) => ({ ...w, isLoading: true }));
await new Promise((r) => setTimeout(r, 1500));
setWizard((w) => ({ ...w, isLoading: true }))
await new Promise((r) => setTimeout(r, 1500))
setWizard((w) => ({
...w,
isLoading: false,
@@ -64,27 +70,25 @@ export function useNuevaAsignaturaWizard(planId: string) {
previewAsignatura: {
nombre: w.datosBasicos.nombre,
objetivo:
"Aplicar los fundamentos teóricos para la resolución de problemas...",
'Aplicar los fundamentos teóricos para la resolución de problemas...',
unidades: 5,
bibliografiaCount: 3,
} as AsignaturaPreview,
},
}));
};
}))
}
const crearAsignatura = async (onCreated: () => void) => {
setWizard((w) => ({ ...w, isLoading: true }));
await new Promise((r) => setTimeout(r, 1000));
onCreated();
};
const crearAsignatura = async () => {
await new Promise((r) => setTimeout(r, 1000))
}
return {
wizard,
setWizard,
canContinueDesdeMetodo,
canContinueDesdeBasicos,
canContinueDesdeConfig,
canContinueDesdeDetalles,
simularGeneracionIA,
crearAsignatura,
};
}
}