From ef177a3f92731fea2ab45b2e1fa43ea3081a11c8 Mon Sep 17 00:00:00 2001 From: Guillermo Arrieta Medina Date: Tue, 6 Jan 2026 13:46:57 -0600 Subject: [PATCH] wip --- .../shadcn-studio/checkbox/checkbox-13.tsx | 32 +++ src/components/shadcn-studio/tabs/tabs-03.tsx | 76 +++++++ src/components/ui/checkbox.tsx | 30 +++ src/components/ui/tabs.tsx | 64 ++++++ .../nueva/NuevaAsignaturaModalContainer.tsx | 127 +++++++++++ src/features/asignaturas/nueva/catalogs.ts | 56 +++++ .../nueva/hooks/useNuevaAsignaturaWizard.ts | 90 ++++++++ src/features/asignaturas/nueva/types.ts | 45 ++++ .../planes/nuevo/NuevoPlanModalContainer.tsx | 205 ++++++++++++++++++ src/features/planes/nuevo/catalogs.ts | 116 ++++++++++ .../planes/nuevo/hooks/useNuevoPlanWizard.ts | 115 ++++++++++ src/features/planes/nuevo/types.ts | 41 ++++ 12 files changed, 997 insertions(+) create mode 100644 src/components/shadcn-studio/checkbox/checkbox-13.tsx create mode 100644 src/components/shadcn-studio/tabs/tabs-03.tsx create mode 100644 src/components/ui/checkbox.tsx create mode 100644 src/components/ui/tabs.tsx create mode 100644 src/features/asignaturas/nueva/NuevaAsignaturaModalContainer.tsx create mode 100644 src/features/asignaturas/nueva/catalogs.ts create mode 100644 src/features/asignaturas/nueva/hooks/useNuevaAsignaturaWizard.ts create mode 100644 src/features/asignaturas/nueva/types.ts create mode 100644 src/features/planes/nuevo/NuevoPlanModalContainer.tsx create mode 100644 src/features/planes/nuevo/catalogs.ts create mode 100644 src/features/planes/nuevo/hooks/useNuevoPlanWizard.ts create mode 100644 src/features/planes/nuevo/types.ts diff --git a/src/components/shadcn-studio/checkbox/checkbox-13.tsx b/src/components/shadcn-studio/checkbox/checkbox-13.tsx new file mode 100644 index 0000000..2c3e8f8 --- /dev/null +++ b/src/components/shadcn-studio/checkbox/checkbox-13.tsx @@ -0,0 +1,32 @@ +import { Checkbox } from '@/components/ui/checkbox' +import { Label } from '@/components/ui/label' + +const CheckboxCardDemo = () => { + return ( +
+ + +
+ ) +} + +export default CheckboxCardDemo diff --git a/src/components/shadcn-studio/tabs/tabs-03.tsx b/src/components/shadcn-studio/tabs/tabs-03.tsx new file mode 100644 index 0000000..f1cd723 --- /dev/null +++ b/src/components/shadcn-studio/tabs/tabs-03.tsx @@ -0,0 +1,76 @@ +import { BookIcon, GiftIcon, HeartIcon } from 'lucide-react' + +import CheckboxCardDemo from '../checkbox/checkbox-13' + +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' + +const tabs = [ + { + name: 'Explore', + value: 'explore', + icon: BookIcon, + content: ( + <> + + + ), + }, + { + name: 'Favorites', + value: 'favorites', + icon: HeartIcon, + content: ( + <> + All your{' '} + favorites are + saved here. Revisit articles, collections, and moments you love, any + time you want a little inspiration. + + ), + }, + { + name: 'Surprise', + value: 'surprise', + icon: GiftIcon, + content: ( + <> + Surprise!{' '} + Here's something unexpected—a fun fact, a quirky tip, or a daily + challenge. Come back for a new surprise every day! + + ), + }, +] + +const TabsWithIconDemo = () => { + return ( +
+ + + {tabs.map(({ icon: Icon, name, value }) => ( + + + {name} + + ))} + + + {tabs.map((tab) => ( + +

{tab.content}

+
+ ))} +
+
+ ) +} + +export default TabsWithIconDemo diff --git a/src/components/ui/checkbox.tsx b/src/components/ui/checkbox.tsx new file mode 100644 index 0000000..0e2a6cd --- /dev/null +++ b/src/components/ui/checkbox.tsx @@ -0,0 +1,30 @@ +import * as React from "react" +import * as CheckboxPrimitive from "@radix-ui/react-checkbox" +import { CheckIcon } from "lucide-react" + +import { cn } from "@/lib/utils" + +function Checkbox({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + + + ) +} + +export { Checkbox } diff --git a/src/components/ui/tabs.tsx b/src/components/ui/tabs.tsx new file mode 100644 index 0000000..eeec6d4 --- /dev/null +++ b/src/components/ui/tabs.tsx @@ -0,0 +1,64 @@ +import * as TabsPrimitive from '@radix-ui/react-tabs' +import * as React from 'react' + +import { cn } from '@/lib/utils' + +function Tabs({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function TabsList({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function TabsTrigger({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function TabsContent({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { Tabs, TabsList, TabsTrigger, TabsContent } diff --git a/src/features/asignaturas/nueva/NuevaAsignaturaModalContainer.tsx b/src/features/asignaturas/nueva/NuevaAsignaturaModalContainer.tsx new file mode 100644 index 0000000..ba66cb1 --- /dev/null +++ b/src/features/asignaturas/nueva/NuevaAsignaturaModalContainer.tsx @@ -0,0 +1,127 @@ +import { useNavigate } from '@tanstack/react-router' + +import { useNuevaAsignaturaWizard } from './hooks/useNuevaAsignaturaWizard' + +import { PasoBasicosForm } from '@/components/asignaturas/wizard/PasoBasicosForm' +import { PasoConfiguracionPanel } from '@/components/asignaturas/wizard/PasoConfiguracionPanel' +import { PasoMetodoCardGroup } from '@/components/asignaturas/wizard/PasoMetodoCardGroup' +import { PasoResumenCard } from '@/components/asignaturas/wizard/PasoResumenCard' +import { VistaSinPermisos } from '@/components/asignaturas/wizard/VistaSinPermisos' +import { WizardControls } from '@/components/asignaturas/wizard/WizardControls' +import { WizardHeader } from '@/components/asignaturas/wizard/WizardHeader' +import { defineStepper } from '@/components/stepper' +import { Dialog, DialogContent } from '@/components/ui/dialog' + +const Wizard = defineStepper( + { + id: 'metodo', + title: 'Método', + description: 'Manual, IA o Clonado', + }, + { + id: 'basicos', + title: 'Datos básicos', + description: 'Nombre y estructura', + }, + { + id: 'configuracion', + title: 'Configuración', + description: 'Detalles según modo', + }, + { + id: 'resumen', + title: 'Resumen', + description: 'Confirmar creación', + }, +) + +const auth_get_current_user_role = () => 'JEFE_CARRERA' as const + +export function NuevaAsignaturaModalContainer({ planId }: { planId: string }) { + const navigate = useNavigate() + const role = auth_get_current_user_role() + + const { + wizard, + setWizard, + canContinueDesdeMetodo, + canContinueDesdeBasicos, + canContinueDesdeConfig, + simularGeneracionIA, + crearAsignatura, + } = useNuevaAsignaturaWizard(planId) + + const handleClose = () => { + navigate({ to: `/planes/${planId}/asignaturas`, resetScroll: false }) + } + + return ( + !open && handleClose()}> + e.preventDefault()} + > + {role !== 'JEFE_CARRERA' ? ( + + ) : ( + + {({ methods }) => ( + <> + + +
+
+ {Wizard.utils.getIndex(methods.current.id) === 0 && ( + + + + )} + {Wizard.utils.getIndex(methods.current.id) === 1 && ( + + + + )} + {Wizard.utils.getIndex(methods.current.id) === 2 && ( + + + + )} + {Wizard.utils.getIndex(methods.current.id) === 3 && ( + + + + )} +
+
+ + crearAsignatura(handleClose)} + /> + + )} +
+ )} +
+
+ ) +} diff --git a/src/features/asignaturas/nueva/catalogs.ts b/src/features/asignaturas/nueva/catalogs.ts new file mode 100644 index 0000000..97ea60a --- /dev/null +++ b/src/features/asignaturas/nueva/catalogs.ts @@ -0,0 +1,56 @@ +import type { TipoAsignatura } from "./types"; + +export const ESTRUCTURAS_SEP = [ + { id: "sep-lic-2025", label: "Licenciatura SEP v2025" }, + { id: "sep-pos-2023", label: "Posgrado SEP v2023" }, + { id: "ulsa-int-2024", label: "Estándar Interno ULSA 2024" }, +]; + +export const TIPOS_MATERIA: Array<{ value: TipoAsignatura; label: string }> = [ + { value: "OBLIGATORIA", label: "Obligatoria" }, + { value: "OPTATIVA", label: "Optativa" }, + { value: "TRONCAL", label: "Troncal / Eje común" }, + { value: "OTRO", label: "Otro" }, +]; + +export const FACULTADES = [ + { id: "ing", nombre: "Facultad de Ingeniería" }, + { id: "med", nombre: "Facultad de Medicina" }, + { id: "neg", nombre: "Facultad de Negocios" }, +]; + +export const CARRERAS = [ + { id: "sis", nombre: "Ing. en Sistemas", facultadId: "ing" }, + { id: "ind", nombre: "Ing. Industrial", facultadId: "ing" }, + { id: "medico", nombre: "Médico Cirujano", facultadId: "med" }, + { id: "act", nombre: "Actuaría", facultadId: "neg" }, +]; + +export const PLANES_MOCK = [ + { id: "p1", nombre: "Plan 2010 Sistemas", carreraId: "sis" }, + { id: "p2", nombre: "Plan 2016 Sistemas", carreraId: "sis" }, + { id: "p3", nombre: "Plan 2015 Industrial", carreraId: "ind" }, +]; + +export const MATERIAS_MOCK = [ + { + id: "m1", + nombre: "Programación Orientada a Objetos", + creditos: 8, + clave: "POO-101", + }, + { id: "m2", nombre: "Cálculo Diferencial", creditos: 6, clave: "MAT-101" }, + { id: "m3", nombre: "Ética Profesional", creditos: 4, clave: "HUM-302" }, + { + id: "m4", + nombre: "Bases de Datos Avanzadas", + creditos: 8, + clave: "BD-201", + }, +]; + +export const ARCHIVOS_SISTEMA_MOCK = [ + { id: "doc1", name: "Sílabo_Base_Ingenieria.pdf" }, + { id: "doc2", name: "Competencias_Egreso_2025.docx" }, + { id: "doc3", name: "Reglamento_Academico.pdf" }, +]; diff --git a/src/features/asignaturas/nueva/hooks/useNuevaAsignaturaWizard.ts b/src/features/asignaturas/nueva/hooks/useNuevaAsignaturaWizard.ts new file mode 100644 index 0000000..dd5c14c --- /dev/null +++ b/src/features/asignaturas/nueva/hooks/useNuevaAsignaturaWizard.ts @@ -0,0 +1,90 @@ +import { useState } from "react"; + +import type { AsignaturaPreview, NewSubjectWizardState } from "../types"; + +export function useNuevaAsignaturaWizard(planId: string) { + const [wizard, setWizard] = useState({ + step: 1, + planId, + modoCreacion: null, + datosBasicos: { + nombre: "", + clave: "", + tipo: "OBLIGATORIA", + creditos: 0, + horasSemana: 0, + estructuraId: "", + }, + clonInterno: {}, + clonTradicional: { + archivoWordAsignaturaId: null, + archivosAdicionalesIds: [], + }, + iaConfig: { + descripcionEnfoque: "", + notasAdicionales: "", + archivosExistentesIds: [], + }, + resumen: {}, + isLoading: false, + errorMessage: null, + }); + + const canContinueDesdeMetodo = wizard.modoCreacion === "MANUAL" || + wizard.modoCreacion === "IA" || + (wizard.modoCreacion === "CLONADO" && !!wizard.subModoClonado); + + const canContinueDesdeBasicos = !!wizard.datosBasicos.nombre && + wizard.datosBasicos.creditos > 0 && + !!wizard.datosBasicos.estructuraId; + + const canContinueDesdeConfig = (() => { + if (wizard.modoCreacion === "MANUAL") return true; + if (wizard.modoCreacion === "IA") { + return !!wizard.iaConfig?.descripcionEnfoque; + } + if (wizard.modoCreacion === "CLONADO") { + if (wizard.subModoClonado === "INTERNO") { + return !!wizard.clonInterno?.asignaturaOrigenId; + } + if (wizard.subModoClonado === "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: false, + resumen: { + previewAsignatura: { + nombre: w.datosBasicos.nombre, + objetivo: + "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(); + }; + + return { + wizard, + setWizard, + canContinueDesdeMetodo, + canContinueDesdeBasicos, + canContinueDesdeConfig, + simularGeneracionIA, + crearAsignatura, + }; +} diff --git a/src/features/asignaturas/nueva/types.ts b/src/features/asignaturas/nueva/types.ts new file mode 100644 index 0000000..799b0d5 --- /dev/null +++ b/src/features/asignaturas/nueva/types.ts @@ -0,0 +1,45 @@ +export type ModoCreacion = "MANUAL" | "IA" | "CLONADO"; +export type SubModoClonado = "INTERNO" | "TRADICIONAL"; +export type TipoAsignatura = "OBLIGATORIA" | "OPTATIVA" | "TRONCAL" | "OTRO"; + +export type AsignaturaPreview = { + nombre: string; + objetivo: string; + unidades: number; + bibliografiaCount: number; +}; + +export type NewSubjectWizardState = { + step: 1 | 2 | 3 | 4; + planId: string; + modoCreacion: ModoCreacion | null; + subModoClonado?: SubModoClonado; + datosBasicos: { + nombre: string; + clave?: string; + tipo: TipoAsignatura; + creditos: number; + horasSemana?: number; + estructuraId: string; + }; + clonInterno?: { + facultadId?: string; + carreraId?: string; + planOrigenId?: string; + asignaturaOrigenId?: string | null; + }; + clonTradicional?: { + archivoWordAsignaturaId: string | null; + archivosAdicionalesIds: Array; + }; + iaConfig?: { + descripcionEnfoque: string; + notasAdicionales: string; + archivosExistentesIds: Array; + }; + resumen: { + previewAsignatura?: AsignaturaPreview; + }; + isLoading: boolean; + errorMessage: string | null; +}; diff --git a/src/features/planes/nuevo/NuevoPlanModalContainer.tsx b/src/features/planes/nuevo/NuevoPlanModalContainer.tsx new file mode 100644 index 0000000..8f985da --- /dev/null +++ b/src/features/planes/nuevo/NuevoPlanModalContainer.tsx @@ -0,0 +1,205 @@ +import { useNavigate } from '@tanstack/react-router' +import * as Icons from 'lucide-react' + +import { useNuevoPlanWizard } from './hooks/useNuevoPlanWizard' + +import { PasoBasicosForm } from '@/components/planes/wizard/PasoBasicosForm' +import { PasoDetallesPanel } from '@/components/planes/wizard/PasoDetallesPanel' +import { PasoModoCardGroup } from '@/components/planes/wizard/PasoModoCardGroup' +import { PasoResumenCard } from '@/components/planes/wizard/PasoResumenCard' +import { WizardControls } from '@/components/planes/wizard/WizardControls' +import { WizardHeader } from '@/components/planes/wizard/WizardHeader' +import { defineStepper } from '@/components/stepper' +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from '@/components/ui/card' +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog' + +// Mock de permisos/rol +const auth_get_current_user_role = () => 'JEFE_CARRERA' as const + +const Wizard = defineStepper( + { + id: 'modo', + title: 'Método', + description: 'Selecciona cómo crearás el plan', + }, + { + id: 'basicos', + title: 'Datos básicos', + description: 'Nombre, carrera, nivel y ciclos', + }, + { id: 'detalles', title: 'Detalles', description: 'IA, clonado o archivos' }, + { id: 'resumen', title: 'Resumen', description: 'Confirma y crea el plan' }, +) + +export default function NuevoPlanModalContainer() { + const navigate = useNavigate() + const role = auth_get_current_user_role() + + const { + wizard, + setWizard, + carrerasFiltradas, + canContinueDesdeModo, + canContinueDesdeBasicos, + canContinueDesdeDetalles, + generarPreviewIA, + } = useNuevoPlanWizard() + + const handleClose = () => { + navigate({ to: '/planes', resetScroll: false }) + } + + const crearPlan = async () => { + setWizard((w) => ({ ...w, isLoading: true, errorMessage: null })) + await new Promise((r) => setTimeout(r, 900)) + const nuevoId = (() => { + if (wizard.modoCreacion === 'MANUAL') return 'plan_new_manual_001' + if (wizard.modoCreacion === 'IA') return 'plan_new_ai_001' + if (wizard.subModoClonado === 'INTERNO') return 'plan_new_clone_001' + return 'plan_new_import_001' + })() + navigate({ to: `/planes/${nuevoId}` }) + } + + return ( + !open && handleClose()}> + { + e.preventDefault() + }} + > + {role !== 'JEFE_CARRERA' ? ( + <> + + Nuevo plan de estudios + +
+ + + + + Sin permisos + + + No tienes permisos para crear planes de estudio. + + + + + + +
+ + ) : ( + + {({ methods }) => { + const currentIndex = Wizard.utils.getIndex(methods.current.id) + 1 + const totalSteps = Wizard.steps.length + const nextStep = Wizard.steps[currentIndex] + + return ( + <> + + +
+
+ {Wizard.utils.getIndex(methods.current.id) === 0 && ( + + + + )} + {Wizard.utils.getIndex(methods.current.id) === 1 && ( + + + + )} + {Wizard.utils.getIndex(methods.current.id) === 2 && ( + + + + )} + {Wizard.utils.getIndex(methods.current.id) === 3 && ( + + + + )} +
+
+ +
+ + methods.prev()} + onNext={() => methods.next()} + onCreate={crearPlan} + disablePrev={ + Wizard.utils.getIndex(methods.current.id) === 0 || + wizard.isLoading + } + disableNext={ + wizard.isLoading || + (Wizard.utils.getIndex(methods.current.id) === 0 && + !canContinueDesdeModo) || + (Wizard.utils.getIndex(methods.current.id) === 1 && + !canContinueDesdeBasicos) || + (Wizard.utils.getIndex(methods.current.id) === 2 && + !canContinueDesdeDetalles) + } + disableCreate={wizard.isLoading} + isLastStep={ + Wizard.utils.getIndex(methods.current.id) >= + Wizard.steps.length - 1 + } + /> + +
+ + ) + }} +
+ )} +
+
+ ) +} diff --git a/src/features/planes/nuevo/catalogs.ts b/src/features/planes/nuevo/catalogs.ts new file mode 100644 index 0000000..28c3d57 --- /dev/null +++ b/src/features/planes/nuevo/catalogs.ts @@ -0,0 +1,116 @@ +import type { TipoCiclo } from "./types"; + +export const FACULTADES = [ + { id: "ing", nombre: "Facultad de Ingeniería" }, + { + id: "med", + nombre: "Facultad de Medicina en medicina en medicina en medicina", + }, + { id: "neg", nombre: "Facultad de Negocios" }, +]; + +export const CARRERAS = [ + { id: "sis", nombre: "Ing. en Sistemas", facultadId: "ing" }, + { id: "ind", nombre: "Ing. Industrial", facultadId: "ing" }, + { id: "medico", nombre: "Médico Cirujano", facultadId: "med" }, + { id: "act", nombre: "Actuaría", facultadId: "neg" }, +]; + +export const NIVELES = [ + "Licenciatura", + "Especialidad", + "Maestría", + "Doctorado", +]; +export const TIPOS_CICLO: Array<{ value: TipoCiclo; label: string }> = [ + { value: "SEMESTRE", label: "Semestre" }, + { value: "CUATRIMESTRE", label: "Cuatrimestre" }, + { value: "TRIMESTRE", label: "Trimestre" }, +]; + +export const PLANES_EXISTENTES = [ + { + id: "plan-2021-sis", + nombre: "ISC 2021", + estado: "Aprobado", + anio: 2021, + facultadId: "ing", + carreraId: "sis", + }, + { + id: "plan-2020-ind", + nombre: "I. Industrial 2020", + estado: "Aprobado", + anio: 2020, + facultadId: "ing", + carreraId: "ind", + }, + { + id: "plan-2019-med", + nombre: "Medicina 2019", + estado: "Vigente", + anio: 2019, + facultadId: "med", + carreraId: "medico", + }, +]; + +export const ARCHIVOS = [ + { + id: "file-1", + nombre: "Sílabo POO 2023.docx", + tipo: "docx", + tamaño: "245 KB", + }, + { + id: "file-2", + nombre: "Guía de prácticas BD.pdf", + tipo: "pdf", + tamaño: "1.2 MB", + }, + { + id: "file-3", + nombre: "Rúbrica evaluación proyectos.xlsx", + tipo: "xlsx", + tamaño: "89 KB", + }, + { + id: "file-4", + nombre: "Banco de reactivos IA.docx", + tipo: "docx", + tamaño: "567 KB", + }, + { + id: "file-5", + nombre: "Material didáctico Web.pdf", + tipo: "pdf", + tamaño: "3.4 MB", + }, +]; + +export const REPOSITORIOS = [ + { + id: "repo-1", + nombre: "Materiales ISC 2024", + descripcion: "Documentos de referencia para Ingeniería en Sistemas", + cantidadArchivos: 45, + }, + { + id: "repo-2", + nombre: "Lineamientos SEP", + descripcion: "Documentos oficiales y normativas SEP actualizadas", + cantidadArchivos: 12, + }, + { + id: "repo-3", + nombre: "Bibliografía Digital", + descripcion: "Recursos bibliográficos digitalizados", + cantidadArchivos: 128, + }, + { + id: "repo-4", + nombre: "Plantillas Institucionales", + descripcion: "Formatos y plantillas oficiales ULSA", + cantidadArchivos: 23, + }, +]; diff --git a/src/features/planes/nuevo/hooks/useNuevoPlanWizard.ts b/src/features/planes/nuevo/hooks/useNuevoPlanWizard.ts new file mode 100644 index 0000000..b94d332 --- /dev/null +++ b/src/features/planes/nuevo/hooks/useNuevoPlanWizard.ts @@ -0,0 +1,115 @@ +import { useMemo, useState } from "react"; + +import { CARRERAS } from "../catalogs"; + +import type { NewPlanWizardState, PlanPreview, TipoCiclo } from "../types"; + +export function useNuevoPlanWizard() { + const [wizard, setWizard] = useState({ + step: 1, + modoCreacion: null, + datosBasicos: { + nombrePlan: "", + carreraId: "", + facultadId: "", + nivel: "", + tipoCiclo: "", + numCiclos: undefined, + }, + clonInterno: { planOrigenId: null }, + clonTradicional: { + archivoWordPlanId: null, + archivoMapaExcelId: null, + archivoAsignaturasExcelId: null, + }, + iaConfig: { + descripcionEnfoque: "", + poblacionObjetivo: "", + notasAdicionales: "", + archivosReferencia: [], + }, + resumen: {}, + isLoading: false, + errorMessage: null, + }); + + const carrerasFiltradas = useMemo(() => { + const fac = wizard.datosBasicos.facultadId; + return fac ? CARRERAS.filter((c) => c.facultadId === fac) : CARRERAS; + }, [wizard.datosBasicos.facultadId]); + + const canContinueDesdeModo = wizard.modoCreacion === "MANUAL" || + wizard.modoCreacion === "IA" || + (wizard.modoCreacion === "CLONADO" && !!wizard.subModoClonado); + + const canContinueDesdeBasicos = !!wizard.datosBasicos.nombrePlan && + !!wizard.datosBasicos.carreraId && + !!wizard.datosBasicos.facultadId && + !!wizard.datosBasicos.nivel && + (wizard.datosBasicos.numCiclos !== undefined && + wizard.datosBasicos.numCiclos > 0); + + const canContinueDesdeDetalles = (() => { + if (wizard.modoCreacion === "MANUAL") return true; + if (wizard.modoCreacion === "IA") { + return !!wizard.iaConfig?.descripcionEnfoque; + } + if (wizard.modoCreacion === "CLONADO") { + if (wizard.subModoClonado === "INTERNO") { + return !!wizard.clonInterno?.planOrigenId; + } + if (wizard.subModoClonado === "TRADICIONAL") { + const t = wizard.clonTradicional; + if (!t) return false; + const tieneWord = !!t.archivoWordPlanId; + const tieneAlMenosUnExcel = !!t.archivoMapaExcelId || + !!t.archivoAsignaturasExcelId; + return tieneWord && tieneAlMenosUnExcel; + } + } + return false; + })(); + + const generarPreviewIA = async () => { + setWizard((w) => ({ ...w, isLoading: true, errorMessage: null })); + await new Promise((r) => setTimeout(r, 800)); + // Ensure preview has the stricter types required by `PlanPreview`. + let tipoCicloSafe: TipoCiclo; + if (wizard.datosBasicos.tipoCiclo === "") { + tipoCicloSafe = "SEMESTRE"; + } else { + tipoCicloSafe = wizard.datosBasicos.tipoCiclo; + } + const numCiclosSafe: number = + typeof wizard.datosBasicos.numCiclos === "number" + ? wizard.datosBasicos.numCiclos + : 1; + + const preview: PlanPreview = { + nombrePlan: wizard.datosBasicos.nombrePlan || "Plan sin nombre", + nivel: wizard.datosBasicos.nivel || "Licenciatura", + tipoCiclo: tipoCicloSafe, + numCiclos: numCiclosSafe, + numAsignaturasAprox: numCiclosSafe * 6, + secciones: [ + { id: "obj", titulo: "Objetivos", resumen: "Borrador de objetivos…" }, + { id: "perfil", titulo: "Perfil de egreso", resumen: "Borrador…" }, + ], + }; + setWizard((w) => ({ + ...w, + isLoading: false, + resumen: { previewPlan: preview }, + })); + }; + + return { + wizard, + setWizard, + carrerasFiltradas, + canContinueDesdeModo, + canContinueDesdeBasicos, + canContinueDesdeDetalles, + generarPreviewIA, + }; +} diff --git a/src/features/planes/nuevo/types.ts b/src/features/planes/nuevo/types.ts new file mode 100644 index 0000000..49c08b1 --- /dev/null +++ b/src/features/planes/nuevo/types.ts @@ -0,0 +1,41 @@ +export type TipoCiclo = "SEMESTRE" | "CUATRIMESTRE" | "TRIMESTRE"; +export type ModoCreacion = "MANUAL" | "IA" | "CLONADO"; +export type SubModoClonado = "INTERNO" | "TRADICIONAL"; + +export type PlanPreview = { + nombrePlan: string; + nivel: string; + tipoCiclo: TipoCiclo; + numCiclos: number; + numAsignaturasAprox?: number; + secciones?: Array<{ id: string; titulo: string; resumen: string }>; +}; + +export type NewPlanWizardState = { + step: 1 | 2 | 3 | 4; + modoCreacion: ModoCreacion | null; + subModoClonado?: SubModoClonado; + datosBasicos: { + nombrePlan: string; + carreraId: string; + facultadId: string; + nivel: string; + tipoCiclo: TipoCiclo | ""; + numCiclos: number | undefined; + }; + clonInterno?: { planOrigenId: string | null }; + clonTradicional?: { + archivoWordPlanId: string | null; + archivoMapaExcelId: string | null; + archivoAsignaturasExcelId: string | null; + }; + iaConfig?: { + descripcionEnfoque: string; + poblacionObjetivo: string; + notasAdicionales: string; + archivosReferencia: Array; + }; + resumen: { previewPlan?: PlanPreview }; + isLoading: boolean; + errorMessage: string | null; +};