From 729051ff87afd889d059ecafadce187ad9baad95 Mon Sep 17 00:00:00 2001 From: Guillermo Arrieta Medina Date: Tue, 3 Feb 2026 13:01:51 -0600 Subject: [PATCH] =?UTF-8?q?Refactorizaci=C3=B3n=20de=20wizards=20para=20co?= =?UTF-8?q?nsistencia,=20reusabilidad=20y=20mantenibilidad?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../asignaturas/wizard/WizardControls.tsx | 64 +++-- src/components/wizard/StepWithTooltip.tsx | 41 ++++ src/components/wizard/WizardLayout.tsx | 52 ++++ .../wizard/WizardResponsiveHeader.tsx | 59 +++++ .../nueva/NuevaAsignaturaModalContainer.tsx | 169 +++++++------ .../planes/nuevo/NuevoPlanModalContainer.tsx | 228 +++++++----------- 6 files changed, 366 insertions(+), 247 deletions(-) create mode 100644 src/components/wizard/StepWithTooltip.tsx create mode 100644 src/components/wizard/WizardLayout.tsx create mode 100644 src/components/wizard/WizardResponsiveHeader.tsx diff --git a/src/components/asignaturas/wizard/WizardControls.tsx b/src/components/asignaturas/wizard/WizardControls.tsx index 9e39b0e..2b62c5f 100644 --- a/src/components/asignaturas/wizard/WizardControls.tsx +++ b/src/components/asignaturas/wizard/WizardControls.tsx @@ -23,43 +23,41 @@ export function WizardControls({ const isLast = idx >= Wizard.steps.length - 1 return ( -
-
-
- {wizard.errorMessage && ( - - {wizard.errorMessage} - - )} -
+
+
+ {wizard.errorMessage && ( + + {wizard.errorMessage} + + )} +
-
+
+ + + {!isLast ? ( - - {!isLast ? ( - - ) : ( - - )} -
+ ) : ( + + )}
) diff --git a/src/components/wizard/StepWithTooltip.tsx b/src/components/wizard/StepWithTooltip.tsx new file mode 100644 index 0000000..44f8e6e --- /dev/null +++ b/src/components/wizard/StepWithTooltip.tsx @@ -0,0 +1,41 @@ +import { useState } from 'react' + +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from '@/components/ui/tooltip' + +export function StepWithTooltip({ + title, + desc, +}: { + title: string + desc: string +}) { + const [isOpen, setIsOpen] = useState(false) + + return ( + + + + { + e.stopPropagation() + setIsOpen((prev) => !prev) + }} + onMouseEnter={() => setIsOpen(true)} + onMouseLeave={() => setIsOpen(false)} + > + {title} + + + +

{desc}

+
+
+
+ ) +} diff --git a/src/components/wizard/WizardLayout.tsx b/src/components/wizard/WizardLayout.tsx new file mode 100644 index 0000000..96fb1a5 --- /dev/null +++ b/src/components/wizard/WizardLayout.tsx @@ -0,0 +1,52 @@ +import * as Icons from 'lucide-react' + +import { CardHeader, CardTitle } from '@/components/ui/card' +import { Dialog, DialogContent } from '@/components/ui/dialog' + +export function WizardLayout({ + title, + onClose, + headerSlot, + footerSlot, + children, +}: { + title: string + onClose: () => void + headerSlot?: React.ReactNode + footerSlot?: React.ReactNode + children: React.ReactNode +}) { + return ( + !open && onClose()}> + { + e.preventDefault() + }} + > +
+ + {title} + + + + {headerSlot ?
{headerSlot}
: null} +
+ +
+ {children} +
+ + {footerSlot ? ( +
{footerSlot}
+ ) : null} +
+
+ ) +} diff --git a/src/components/wizard/WizardResponsiveHeader.tsx b/src/components/wizard/WizardResponsiveHeader.tsx new file mode 100644 index 0000000..7766e13 --- /dev/null +++ b/src/components/wizard/WizardResponsiveHeader.tsx @@ -0,0 +1,59 @@ +import { CircularProgress } from '@/components/CircularProgress' +import { StepWithTooltip } from '@/components/wizard/StepWithTooltip' + +export function WizardResponsiveHeader({ + wizard, + methods, +}: { + wizard: any + methods: any +}) { + const idx = wizard.utils.getIndex(methods.current.id) + const totalSteps = wizard.steps.length + const currentIndex = idx + 1 + const hasNextStep = idx < totalSteps - 1 + const nextStep = wizard.steps[currentIndex] + + return ( + <> +
+
+ +
+

+ +

+ {hasNextStep && nextStep ? ( +

+ Siguiente: {nextStep.title} +

+ ) : ( +

+ ¡Último paso! +

+ )} +
+
+
+ +
+ + {wizard.steps.map((step: any) => ( + + + + + + ))} + +
+ + ) +} diff --git a/src/features/asignaturas/nueva/NuevaAsignaturaModalContainer.tsx b/src/features/asignaturas/nueva/NuevaAsignaturaModalContainer.tsx index e7b9fe0..23758c8 100644 --- a/src/features/asignaturas/nueva/NuevaAsignaturaModalContainer.tsx +++ b/src/features/asignaturas/nueva/NuevaAsignaturaModalContainer.tsx @@ -1,4 +1,5 @@ import { useNavigate } from '@tanstack/react-router' +import * as Icons from 'lucide-react' import { useNuevaAsignaturaWizard } from './hooks/useNuevaAsignaturaWizard' @@ -6,13 +7,20 @@ 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' +import { Button } from '@/components/ui/button' +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from '@/components/ui/card' +import { WizardLayout } from '@/components/wizard/WizardLayout' +import { WizardResponsiveHeader } from '@/components/wizard/WizardResponsiveHeader' -const auth_get_current_user_role = () => 'JEFE_CARRERA' as const +const auth_get_current_user_role = (): string => 'JEFE_CARRERA' const Wizard = defineStepper( { @@ -55,85 +63,90 @@ export function NuevaAsignaturaModalContainer({ planId }: { planId: string }) { navigate({ to: `/planes/${planId}/asignaturas`, resetScroll: false }) } + if (role !== 'JEFE_CARRERA') { + return ( + + + + + + Sin permisos + + + Solo el Jefe de Carrera puede crear asignaturas. + + + + + + + + ) + } + return ( - !open && handleClose()}> - e.preventDefault()} - > - {role !== 'JEFE_CARRERA' ? ( - - ) : ( - + {({ methods }) => { + const idx = Wizard.utils.getIndex(methods.current.id) + + return ( + + } + footerSlot={ + + crearAsignatura(handleClose)} + /> + + } > - {({ methods }) => { - const currentIndex = Wizard.utils.getIndex(methods.current.id) + 1 - const totalSteps = Wizard.steps.length - const nextStep = Wizard.steps[currentIndex] ?? { - title: '', - description: '', - } +
+ {idx === 0 && ( + + + + )} - return ( - <> - + {idx === 1 && ( + + + + )} -
-
- {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)} + onChange={setWizard} + onGenerarIA={simularGeneracionIA} /> - - ) - }} - - )} - -
+ + )} + + {idx === 3 && ( + + + + )} +
+ + ) + }} + ) } diff --git a/src/features/planes/nuevo/NuevoPlanModalContainer.tsx b/src/features/planes/nuevo/NuevoPlanModalContainer.tsx index 41062e2..609df22 100644 --- a/src/features/planes/nuevo/NuevoPlanModalContainer.tsx +++ b/src/features/planes/nuevo/NuevoPlanModalContainer.tsx @@ -10,7 +10,6 @@ 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, @@ -19,16 +18,12 @@ import { CardHeader, CardTitle, } from '@/components/ui/card' -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, -} from '@/components/ui/dialog' +import { WizardLayout } from '@/components/wizard/WizardLayout' +import { WizardResponsiveHeader } from '@/components/wizard/WizardResponsiveHeader' // import { useGeneratePlanAI } from '@/data/hooks/usePlans' // Mock de permisos/rol -const auth_get_current_user_role = () => 'JEFE_CARRERA' as const +const auth_get_current_user_role = (): string => 'JEFE_CARRERA' const Wizard = defineStepper( { @@ -64,136 +59,97 @@ export default function NuevoPlanModalContainer() { // Crear plan: ahora la lógica vive en WizardControls + if (role !== 'JEFE_CARRERA') { + return ( + + + + + + Sin permisos + + + No tienes permisos para crear planes de estudio. + + + + + + + + ) + } + return ( - !open && handleClose()}> - { - e.preventDefault() - }} - > - {role !== 'JEFE_CARRERA' ? ( - <> - - Nuevo plan de estudios - -
- - - - - Sin permisos - - - No tienes permisos para crear planes de estudio. - - - - - - -
- - ) : ( - + {({ methods }) => { + const idx = Wizard.utils.getIndex(methods.current.id) + + return ( + + } + footerSlot={ + + methods.prev()} + onNext={() => methods.next()} + disablePrev={idx === 0 || wizard.isLoading} + disableNext={ + wizard.isLoading || + (idx === 0 && !canContinueDesdeModo) || + (idx === 1 && !canContinueDesdeBasicos) || + (idx === 2 && !canContinueDesdeDetalles) + } + disableCreate={wizard.isLoading} + isLastStep={idx >= Wizard.steps.length - 1} + wizard={wizard} + setWizard={setWizard} + /> + + } > - {({ methods }) => { - const currentIndex = Wizard.utils.getIndex(methods.current.id) + 1 - const totalSteps = Wizard.steps.length - const nextStep = Wizard.steps[currentIndex] ?? { - title: '', - description: '', - } - - return ( - <> - + {idx === 0 && ( + + + + )} + {idx === 1 && ( + + + + )} + {idx === 2 && ( + + - -
-
- {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()} - 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 - } - wizard={wizard} - setWizard={setWizard} - /> - -
- - ) - }} -
- )} -
-
+ + )} + {idx === 3 && ( + + + + )} +
+ + ) + }} + ) }