diff --git a/src/components/CircularProgress.tsx b/src/components/CircularProgress.tsx new file mode 100644 index 0000000..a35c018 --- /dev/null +++ b/src/components/CircularProgress.tsx @@ -0,0 +1,74 @@ +import { cn } from '@/lib/utils' + +interface CircularProgressProps { + current: number + total: number + className?: string +} + +export function CircularProgress({ + current, + total, + className, +}: CircularProgressProps) { + // Configuración interna del SVG (Coordenadas 100x100) + const center = 50 + const strokeWidth = 8 // Grosor de la línea + const radius = 40 // Radio (dejamos margen para el borde) + const circumference = 2 * Math.PI * radius + + // Cálculo del porcentaje inverso (para que se llene correctamente) + const percentage = (current / total) * 100 + const strokeDashoffset = circumference - (percentage / 100) * circumference + + return ( + // CAMBIO CLAVE 1: 'size-24' (96px) da mucho más aire que 'size-16' +
+ {/* CAMBIO CLAVE 2: Contenedor de texto con inset-0 para centrado perfecto */} +
+ + Paso + + + {current}{' '} + + / {total} + + +
+ + {/* SVG con viewBox para escalar automáticamente */} + + {/* Círculo de Fondo (Gris claro) */} + + {/* Círculo de Progreso (Verde/Color principal) */} + + +
+ ) +} diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index 0222f41..0858c7d 100644 --- a/src/routeTree.gen.ts +++ b/src/routeTree.gen.ts @@ -9,6 +9,7 @@ // Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. import { Route as rootRouteImport } from './routes/__root' +import { Route as Stepper2RouteImport } from './routes/stepper2' import { Route as StepperRouteImport } from './routes/stepper' import { Route as LoginRouteImport } from './routes/login' import { Route as DashboardRouteImport } from './routes/dashboard' @@ -18,6 +19,11 @@ import { Route as PlanesListaRouteRouteImport } from './routes/planes/_lista/rou import { Route as PlanesPlanIdRouteRouteImport } from './routes/planes/$planId/route' import { Route as PlanesListaNuevoRouteImport } from './routes/planes/_lista/nuevo' +const Stepper2Route = Stepper2RouteImport.update({ + id: '/stepper2', + path: '/stepper2', + getParentRoute: () => rootRouteImport, +} as any) const StepperRoute = StepperRouteImport.update({ id: '/stepper', path: '/stepper', @@ -64,6 +70,7 @@ export interface FileRoutesByFullPath { '/dashboard': typeof DashboardRoute '/login': typeof LoginRoute '/stepper': typeof StepperRoute + '/stepper2': typeof Stepper2Route '/planes/$planId': typeof PlanesPlanIdRouteRoute '/planes': typeof PlanesListaRouteRouteWithChildren '/demo/tanstack-query': typeof DemoTanstackQueryRoute @@ -74,6 +81,7 @@ export interface FileRoutesByTo { '/dashboard': typeof DashboardRoute '/login': typeof LoginRoute '/stepper': typeof StepperRoute + '/stepper2': typeof Stepper2Route '/planes/$planId': typeof PlanesPlanIdRouteRoute '/planes': typeof PlanesListaRouteRouteWithChildren '/demo/tanstack-query': typeof DemoTanstackQueryRoute @@ -85,6 +93,7 @@ export interface FileRoutesById { '/dashboard': typeof DashboardRoute '/login': typeof LoginRoute '/stepper': typeof StepperRoute + '/stepper2': typeof Stepper2Route '/planes/$planId': typeof PlanesPlanIdRouteRoute '/planes/_lista': typeof PlanesListaRouteRouteWithChildren '/demo/tanstack-query': typeof DemoTanstackQueryRoute @@ -97,6 +106,7 @@ export interface FileRouteTypes { | '/dashboard' | '/login' | '/stepper' + | '/stepper2' | '/planes/$planId' | '/planes' | '/demo/tanstack-query' @@ -107,6 +117,7 @@ export interface FileRouteTypes { | '/dashboard' | '/login' | '/stepper' + | '/stepper2' | '/planes/$planId' | '/planes' | '/demo/tanstack-query' @@ -117,6 +128,7 @@ export interface FileRouteTypes { | '/dashboard' | '/login' | '/stepper' + | '/stepper2' | '/planes/$planId' | '/planes/_lista' | '/demo/tanstack-query' @@ -128,6 +140,7 @@ export interface RootRouteChildren { DashboardRoute: typeof DashboardRoute LoginRoute: typeof LoginRoute StepperRoute: typeof StepperRoute + Stepper2Route: typeof Stepper2Route PlanesPlanIdRouteRoute: typeof PlanesPlanIdRouteRoute PlanesListaRouteRoute: typeof PlanesListaRouteRouteWithChildren DemoTanstackQueryRoute: typeof DemoTanstackQueryRoute @@ -135,6 +148,13 @@ export interface RootRouteChildren { declare module '@tanstack/react-router' { interface FileRoutesByPath { + '/stepper2': { + id: '/stepper2' + path: '/stepper2' + fullPath: '/stepper2' + preLoaderRoute: typeof Stepper2RouteImport + parentRoute: typeof rootRouteImport + } '/stepper': { id: '/stepper' path: '/stepper' @@ -210,6 +230,7 @@ const rootRouteChildren: RootRouteChildren = { DashboardRoute: DashboardRoute, LoginRoute: LoginRoute, StepperRoute: StepperRoute, + Stepper2Route: Stepper2Route, PlanesPlanIdRouteRoute: PlanesPlanIdRouteRoute, PlanesListaRouteRoute: PlanesListaRouteRouteWithChildren, DemoTanstackQueryRoute: DemoTanstackQueryRoute, diff --git a/src/routes/planes/_lista/nuevo.tsx b/src/routes/planes/_lista/nuevo.tsx index 2474b22..ee32f3a 100644 --- a/src/routes/planes/_lista/nuevo.tsx +++ b/src/routes/planes/_lista/nuevo.tsx @@ -172,7 +172,7 @@ function NuevoPlanModal() { navigate({ to: '/planes', resetScroll: false }) } - // Derivados + // --- LÓGICA DE VALIDACIÓN Y DATA --- const carrerasFiltradas = useMemo(() => { const fac = wizard.datosBasicos.facultadId return fac ? CARRERAS.filter((c) => c.facultadId === fac) : CARRERAS @@ -202,7 +202,6 @@ function NuevoPlanModal() { if (wizard.subModoClonado === 'TRADICIONAL') { const t = wizard.clonTradicional if (!t) return false - // Reglas mínimas: Word + al menos un Excel const tieneWord = !!t.archivoWordPlanId const tieneAlMenosUnExcel = !!t.archivoMapaExcelId || !!t.archivoMateriasExcelId @@ -236,7 +235,6 @@ function NuevoPlanModal() { const crearPlan = async () => { setWizard((w) => ({ ...w, isLoading: true, errorMessage: null })) await new Promise((r) => setTimeout(r, 900)) - // Elegimos un id ficticio distinto según modo const nuevoId = (() => { if (wizard.modoCreacion === 'MANUAL') return 'plan_new_manual_001' if (wizard.modoCreacion === 'IA') return 'plan_new_ai_001' @@ -245,157 +243,168 @@ function NuevoPlanModal() { })() navigate({ to: `/planes/${nuevoId}` }) } + // ------------------------------------------------ return ( !open && handleClose()}> - - - Nuevo plan de estudios - - + {/* FIX LAYOUT: + 1. h-[90vh]: Altura fija del 90% de la ventana. + 2. flex-col gap-0 p-0: Quitamos espacios automáticos para control manual. + */} + {role !== 'JEFE_CARRERA' ? ( -
- - - - - Sin permisos - - - No tienes permisos para crear planes de estudio. - - - - - - -
+ // --- VISTA SIN PERMISOS --- + <> + + Nuevo plan de estudios + +
+ + + + + Sin permisos + + + No tienes permisos para crear planes de estudio. + + + + + + +
+ ) : ( -
- - {({ methods }) => ( - <> - {/* Header + navegación */} - - - 1. Método - - Selecciona cómo crearás el plan - - - - - 2. Datos básicos - - - Nombre, carrera, nivel y ciclos - - - - 3. Detalles - - IA, clonado o archivos - - - - 4. Resumen - - Confirma y crea el plan - - - + // --- VISTA WIZARD --- + + {({ methods }) => ( + <> + {/* 1. HEADER (FIJO - Top Bun) + flex-none: Asegura que no se aplaste ni haga scroll. + */} +
+ + Nuevo plan de estudios + - {/* Info de paso actual */} -
- - Paso {Wizard.utils.getIndex(methods.current.id) + 1} de{' '} - {Wizard.steps.length} - + {/* Navegación dentro del bloque fijo */} +
+ + {/* Nota: En móvil esto se verá apretado. Considera overflow-x-auto si tienes muchos pasos */} +
+ {Wizard.steps.map((step) => ( + + + {step.title} + + + ))} +
+
+
- {/* Panel activo (solo uno visible) */} -
-
- {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 && ( - - - + {/* 2. CONTENIDO (SCROLLEABLE - Meat) + flex-1: Ocupa todo el espacio restante. + overflow-y-auto: Scrollea solo esta parte. + */} +
+ {/* Aquí renderizamos el panel. Quitamos el 'grid' del padre para evitar conflictos de altura */} +
+ {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 && ( + + + + )} +
+
+ + {/* 3. CONTROLES (FIJO - Bottom Bun) + flex-none: Siempre visible abajo. + */} +
+ +
+ {wizard.errorMessage && ( + + {wizard.errorMessage} + )}
-
- {/* Controles */} - - {wizard.errorMessage && ( - - {wizard.errorMessage} - - )} - - {Wizard.utils.getIndex(methods.current.id) < - Wizard.steps.length - 1 ? ( +
- ) : ( - - )} + + {Wizard.utils.getIndex(methods.current.id) < + Wizard.steps.length - 1 ? ( + + ) : ( + + )} +
- - )} - -
+
+ + )} + )}
@@ -506,7 +515,7 @@ function PasoBasicos({ carrerasFiltradas: typeof CARRERAS }) { return ( -
+
+ {({ methods }) => { + // Calculamos índices para el gráfico + const currentIndex = + methods.all.findIndex((s) => s.id === methods.current.id) + 1 + const totalSteps = methods.all.length + const nextStep = methods.all[currentIndex] // El paso siguiente (si existe) + + return ( +
+ {/* --- AQUÍ ESTÁ LA MAGIA (Tu UI Personalizada) --- */} +
+ {/* El Gráfico Circular */} + + + {/* Los Textos */} +
+

+ {methods.current.title} +

+ {nextStep && ( +

+ Next: {nextStep.title} +

+ )} +
+
+ {/* ----------------------------------------------- */} + + {/* El contenido de los pasos (Switch) */} +
+ {methods.switch({ + contact: () =>
Formulario Contacto...
, + shipping: () =>
Formulario Envío...
, + billing: () =>
Formulario Facturación...
, + review: () =>
Resumen...
, + })} +
+ + {/* Controles de Navegación (Footer) */} +
+ + +
+
+ ) + }} + + ) +}