wip
This commit is contained in:
74
src/components/CircularProgress.tsx
Normal file
74
src/components/CircularProgress.tsx
Normal file
@@ -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'
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'relative flex size-20 items-center justify-center',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{/* CAMBIO CLAVE 2: Contenedor de texto con inset-0 para centrado perfecto */}
|
||||||
|
<div className="absolute inset-0 flex flex-col items-center justify-center">
|
||||||
|
<span className="mb-1 text-sm leading-none font-medium text-slate-500">
|
||||||
|
Paso
|
||||||
|
</span>
|
||||||
|
<span className="text-base leading-none font-bold text-slate-900">
|
||||||
|
{current}{' '}
|
||||||
|
<span className="text-base font-normal text-slate-400">
|
||||||
|
/ {total}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* SVG con viewBox para escalar automáticamente */}
|
||||||
|
<svg className="size-full -rotate-90" viewBox="0 0 100 100">
|
||||||
|
{/* Círculo de Fondo (Gris claro) */}
|
||||||
|
<circle
|
||||||
|
cx={center}
|
||||||
|
cy={center}
|
||||||
|
r={radius}
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth={strokeWidth}
|
||||||
|
fill="transparent"
|
||||||
|
className="text-slate-100"
|
||||||
|
/>
|
||||||
|
{/* Círculo de Progreso (Verde/Color principal) */}
|
||||||
|
<circle
|
||||||
|
cx={center}
|
||||||
|
cy={center}
|
||||||
|
r={radius}
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth={strokeWidth}
|
||||||
|
fill="transparent"
|
||||||
|
strokeDasharray={circumference}
|
||||||
|
strokeDashoffset={strokeDashoffset}
|
||||||
|
strokeLinecap="round"
|
||||||
|
className="text-primary transition-all duration-500 ease-out"
|
||||||
|
// Nota: usa text-primary para tomar el color de tu tema, o pon text-green-500
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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.
|
// 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 rootRouteImport } from './routes/__root'
|
||||||
|
import { Route as Stepper2RouteImport } from './routes/stepper2'
|
||||||
import { Route as StepperRouteImport } from './routes/stepper'
|
import { Route as StepperRouteImport } from './routes/stepper'
|
||||||
import { Route as LoginRouteImport } from './routes/login'
|
import { Route as LoginRouteImport } from './routes/login'
|
||||||
import { Route as DashboardRouteImport } from './routes/dashboard'
|
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 PlanesPlanIdRouteRouteImport } from './routes/planes/$planId/route'
|
||||||
import { Route as PlanesListaNuevoRouteImport } from './routes/planes/_lista/nuevo'
|
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({
|
const StepperRoute = StepperRouteImport.update({
|
||||||
id: '/stepper',
|
id: '/stepper',
|
||||||
path: '/stepper',
|
path: '/stepper',
|
||||||
@@ -64,6 +70,7 @@ export interface FileRoutesByFullPath {
|
|||||||
'/dashboard': typeof DashboardRoute
|
'/dashboard': typeof DashboardRoute
|
||||||
'/login': typeof LoginRoute
|
'/login': typeof LoginRoute
|
||||||
'/stepper': typeof StepperRoute
|
'/stepper': typeof StepperRoute
|
||||||
|
'/stepper2': typeof Stepper2Route
|
||||||
'/planes/$planId': typeof PlanesPlanIdRouteRoute
|
'/planes/$planId': typeof PlanesPlanIdRouteRoute
|
||||||
'/planes': typeof PlanesListaRouteRouteWithChildren
|
'/planes': typeof PlanesListaRouteRouteWithChildren
|
||||||
'/demo/tanstack-query': typeof DemoTanstackQueryRoute
|
'/demo/tanstack-query': typeof DemoTanstackQueryRoute
|
||||||
@@ -74,6 +81,7 @@ export interface FileRoutesByTo {
|
|||||||
'/dashboard': typeof DashboardRoute
|
'/dashboard': typeof DashboardRoute
|
||||||
'/login': typeof LoginRoute
|
'/login': typeof LoginRoute
|
||||||
'/stepper': typeof StepperRoute
|
'/stepper': typeof StepperRoute
|
||||||
|
'/stepper2': typeof Stepper2Route
|
||||||
'/planes/$planId': typeof PlanesPlanIdRouteRoute
|
'/planes/$planId': typeof PlanesPlanIdRouteRoute
|
||||||
'/planes': typeof PlanesListaRouteRouteWithChildren
|
'/planes': typeof PlanesListaRouteRouteWithChildren
|
||||||
'/demo/tanstack-query': typeof DemoTanstackQueryRoute
|
'/demo/tanstack-query': typeof DemoTanstackQueryRoute
|
||||||
@@ -85,6 +93,7 @@ export interface FileRoutesById {
|
|||||||
'/dashboard': typeof DashboardRoute
|
'/dashboard': typeof DashboardRoute
|
||||||
'/login': typeof LoginRoute
|
'/login': typeof LoginRoute
|
||||||
'/stepper': typeof StepperRoute
|
'/stepper': typeof StepperRoute
|
||||||
|
'/stepper2': typeof Stepper2Route
|
||||||
'/planes/$planId': typeof PlanesPlanIdRouteRoute
|
'/planes/$planId': typeof PlanesPlanIdRouteRoute
|
||||||
'/planes/_lista': typeof PlanesListaRouteRouteWithChildren
|
'/planes/_lista': typeof PlanesListaRouteRouteWithChildren
|
||||||
'/demo/tanstack-query': typeof DemoTanstackQueryRoute
|
'/demo/tanstack-query': typeof DemoTanstackQueryRoute
|
||||||
@@ -97,6 +106,7 @@ export interface FileRouteTypes {
|
|||||||
| '/dashboard'
|
| '/dashboard'
|
||||||
| '/login'
|
| '/login'
|
||||||
| '/stepper'
|
| '/stepper'
|
||||||
|
| '/stepper2'
|
||||||
| '/planes/$planId'
|
| '/planes/$planId'
|
||||||
| '/planes'
|
| '/planes'
|
||||||
| '/demo/tanstack-query'
|
| '/demo/tanstack-query'
|
||||||
@@ -107,6 +117,7 @@ export interface FileRouteTypes {
|
|||||||
| '/dashboard'
|
| '/dashboard'
|
||||||
| '/login'
|
| '/login'
|
||||||
| '/stepper'
|
| '/stepper'
|
||||||
|
| '/stepper2'
|
||||||
| '/planes/$planId'
|
| '/planes/$planId'
|
||||||
| '/planes'
|
| '/planes'
|
||||||
| '/demo/tanstack-query'
|
| '/demo/tanstack-query'
|
||||||
@@ -117,6 +128,7 @@ export interface FileRouteTypes {
|
|||||||
| '/dashboard'
|
| '/dashboard'
|
||||||
| '/login'
|
| '/login'
|
||||||
| '/stepper'
|
| '/stepper'
|
||||||
|
| '/stepper2'
|
||||||
| '/planes/$planId'
|
| '/planes/$planId'
|
||||||
| '/planes/_lista'
|
| '/planes/_lista'
|
||||||
| '/demo/tanstack-query'
|
| '/demo/tanstack-query'
|
||||||
@@ -128,6 +140,7 @@ export interface RootRouteChildren {
|
|||||||
DashboardRoute: typeof DashboardRoute
|
DashboardRoute: typeof DashboardRoute
|
||||||
LoginRoute: typeof LoginRoute
|
LoginRoute: typeof LoginRoute
|
||||||
StepperRoute: typeof StepperRoute
|
StepperRoute: typeof StepperRoute
|
||||||
|
Stepper2Route: typeof Stepper2Route
|
||||||
PlanesPlanIdRouteRoute: typeof PlanesPlanIdRouteRoute
|
PlanesPlanIdRouteRoute: typeof PlanesPlanIdRouteRoute
|
||||||
PlanesListaRouteRoute: typeof PlanesListaRouteRouteWithChildren
|
PlanesListaRouteRoute: typeof PlanesListaRouteRouteWithChildren
|
||||||
DemoTanstackQueryRoute: typeof DemoTanstackQueryRoute
|
DemoTanstackQueryRoute: typeof DemoTanstackQueryRoute
|
||||||
@@ -135,6 +148,13 @@ export interface RootRouteChildren {
|
|||||||
|
|
||||||
declare module '@tanstack/react-router' {
|
declare module '@tanstack/react-router' {
|
||||||
interface FileRoutesByPath {
|
interface FileRoutesByPath {
|
||||||
|
'/stepper2': {
|
||||||
|
id: '/stepper2'
|
||||||
|
path: '/stepper2'
|
||||||
|
fullPath: '/stepper2'
|
||||||
|
preLoaderRoute: typeof Stepper2RouteImport
|
||||||
|
parentRoute: typeof rootRouteImport
|
||||||
|
}
|
||||||
'/stepper': {
|
'/stepper': {
|
||||||
id: '/stepper'
|
id: '/stepper'
|
||||||
path: '/stepper'
|
path: '/stepper'
|
||||||
@@ -210,6 +230,7 @@ const rootRouteChildren: RootRouteChildren = {
|
|||||||
DashboardRoute: DashboardRoute,
|
DashboardRoute: DashboardRoute,
|
||||||
LoginRoute: LoginRoute,
|
LoginRoute: LoginRoute,
|
||||||
StepperRoute: StepperRoute,
|
StepperRoute: StepperRoute,
|
||||||
|
Stepper2Route: Stepper2Route,
|
||||||
PlanesPlanIdRouteRoute: PlanesPlanIdRouteRoute,
|
PlanesPlanIdRouteRoute: PlanesPlanIdRouteRoute,
|
||||||
PlanesListaRouteRoute: PlanesListaRouteRouteWithChildren,
|
PlanesListaRouteRoute: PlanesListaRouteRouteWithChildren,
|
||||||
DemoTanstackQueryRoute: DemoTanstackQueryRoute,
|
DemoTanstackQueryRoute: DemoTanstackQueryRoute,
|
||||||
|
|||||||
@@ -172,7 +172,7 @@ function NuevoPlanModal() {
|
|||||||
navigate({ to: '/planes', resetScroll: false })
|
navigate({ to: '/planes', resetScroll: false })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Derivados
|
// --- LÓGICA DE VALIDACIÓN Y DATA ---
|
||||||
const carrerasFiltradas = useMemo(() => {
|
const carrerasFiltradas = useMemo(() => {
|
||||||
const fac = wizard.datosBasicos.facultadId
|
const fac = wizard.datosBasicos.facultadId
|
||||||
return fac ? CARRERAS.filter((c) => c.facultadId === fac) : CARRERAS
|
return fac ? CARRERAS.filter((c) => c.facultadId === fac) : CARRERAS
|
||||||
@@ -202,7 +202,6 @@ function NuevoPlanModal() {
|
|||||||
if (wizard.subModoClonado === 'TRADICIONAL') {
|
if (wizard.subModoClonado === 'TRADICIONAL') {
|
||||||
const t = wizard.clonTradicional
|
const t = wizard.clonTradicional
|
||||||
if (!t) return false
|
if (!t) return false
|
||||||
// Reglas mínimas: Word + al menos un Excel
|
|
||||||
const tieneWord = !!t.archivoWordPlanId
|
const tieneWord = !!t.archivoWordPlanId
|
||||||
const tieneAlMenosUnExcel =
|
const tieneAlMenosUnExcel =
|
||||||
!!t.archivoMapaExcelId || !!t.archivoMateriasExcelId
|
!!t.archivoMapaExcelId || !!t.archivoMateriasExcelId
|
||||||
@@ -236,7 +235,6 @@ function NuevoPlanModal() {
|
|||||||
const crearPlan = async () => {
|
const crearPlan = async () => {
|
||||||
setWizard((w) => ({ ...w, isLoading: true, errorMessage: null }))
|
setWizard((w) => ({ ...w, isLoading: true, errorMessage: null }))
|
||||||
await new Promise((r) => setTimeout(r, 900))
|
await new Promise((r) => setTimeout(r, 900))
|
||||||
// Elegimos un id ficticio distinto según modo
|
|
||||||
const nuevoId = (() => {
|
const nuevoId = (() => {
|
||||||
if (wizard.modoCreacion === 'MANUAL') return 'plan_new_manual_001'
|
if (wizard.modoCreacion === 'MANUAL') return 'plan_new_manual_001'
|
||||||
if (wizard.modoCreacion === 'IA') return 'plan_new_ai_001'
|
if (wizard.modoCreacion === 'IA') return 'plan_new_ai_001'
|
||||||
@@ -245,157 +243,168 @@ function NuevoPlanModal() {
|
|||||||
})()
|
})()
|
||||||
navigate({ to: `/planes/${nuevoId}` })
|
navigate({ to: `/planes/${nuevoId}` })
|
||||||
}
|
}
|
||||||
|
// ------------------------------------------------
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={true} onOpenChange={(open) => !open && handleClose()}>
|
<Dialog open={true} onOpenChange={(open) => !open && handleClose()}>
|
||||||
<DialogContent className="p-0 sm:max-w-[840px]">
|
{/* FIX LAYOUT:
|
||||||
<DialogHeader className="px-6 pt-6">
|
1. h-[90vh]: Altura fija del 90% de la ventana.
|
||||||
<DialogTitle>Nuevo plan de estudios</DialogTitle>
|
2. flex-col gap-0 p-0: Quitamos espacios automáticos para control manual.
|
||||||
</DialogHeader>
|
*/}
|
||||||
|
<DialogContent className="flex h-[90vh] w-full flex-col gap-0 overflow-hidden p-0 sm:max-w-4xl">
|
||||||
{role !== 'JEFE_CARRERA' ? (
|
{role !== 'JEFE_CARRERA' ? (
|
||||||
<div className="px-6 pb-6">
|
// --- VISTA SIN PERMISOS ---
|
||||||
<Card className="border-destructive/40">
|
<>
|
||||||
<CardHeader>
|
<DialogHeader className="flex-none border-b p-6">
|
||||||
<CardTitle className="flex items-center gap-2">
|
<DialogTitle>Nuevo plan de estudios</DialogTitle>
|
||||||
<Icons.ShieldAlert className="text-destructive h-5 w-5" />
|
</DialogHeader>
|
||||||
Sin permisos
|
<div className="flex-1 p-6">
|
||||||
</CardTitle>
|
<Card className="border-destructive/40">
|
||||||
<CardDescription>
|
<CardHeader>
|
||||||
No tienes permisos para crear planes de estudio.
|
<CardTitle className="flex items-center gap-2">
|
||||||
</CardDescription>
|
<Icons.ShieldAlert className="text-destructive h-5 w-5" />
|
||||||
</CardHeader>
|
Sin permisos
|
||||||
<CardContent className="flex justify-end">
|
</CardTitle>
|
||||||
<Button variant="secondary" onClick={handleClose}>
|
<CardDescription>
|
||||||
Volver
|
No tienes permisos para crear planes de estudio.
|
||||||
</Button>
|
</CardDescription>
|
||||||
</CardContent>
|
</CardHeader>
|
||||||
</Card>
|
<CardContent className="flex justify-end">
|
||||||
</div>
|
<Button variant="secondary" onClick={handleClose}>
|
||||||
|
Volver
|
||||||
|
</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div className="px-6 pb-6">
|
// --- VISTA WIZARD ---
|
||||||
<Wizard.Stepper.Provider
|
<Wizard.Stepper.Provider
|
||||||
initialStep={Wizard.utils.getFirst().id}
|
initialStep={Wizard.utils.getFirst().id}
|
||||||
className="flex flex-col gap-6"
|
className="flex h-full flex-col"
|
||||||
>
|
>
|
||||||
{({ methods }) => (
|
{({ methods }) => (
|
||||||
<>
|
<>
|
||||||
{/* Header + navegación */}
|
{/* 1. HEADER (FIJO - Top Bun)
|
||||||
<Wizard.Stepper.Navigation className="border-border/60 rounded-xl border p-4">
|
flex-none: Asegura que no se aplaste ni haga scroll.
|
||||||
<Wizard.Stepper.Step of={Wizard.steps[0].id}>
|
*/}
|
||||||
<Wizard.Stepper.Title>1. Método</Wizard.Stepper.Title>
|
<div className="z-10 flex-none border-b bg-white">
|
||||||
<Wizard.Stepper.Description>
|
<DialogHeader className="p-6 pb-4">
|
||||||
Selecciona cómo crearás el plan
|
<DialogTitle>Nuevo plan de estudios</DialogTitle>
|
||||||
</Wizard.Stepper.Description>
|
</DialogHeader>
|
||||||
</Wizard.Stepper.Step>
|
|
||||||
<Wizard.Stepper.Step of={Wizard.steps[1].id}>
|
|
||||||
<Wizard.Stepper.Title>
|
|
||||||
2. Datos básicos
|
|
||||||
</Wizard.Stepper.Title>
|
|
||||||
<Wizard.Stepper.Description>
|
|
||||||
Nombre, carrera, nivel y ciclos
|
|
||||||
</Wizard.Stepper.Description>
|
|
||||||
</Wizard.Stepper.Step>
|
|
||||||
<Wizard.Stepper.Step of={Wizard.steps[2].id}>
|
|
||||||
<Wizard.Stepper.Title>3. Detalles</Wizard.Stepper.Title>
|
|
||||||
<Wizard.Stepper.Description>
|
|
||||||
IA, clonado o archivos
|
|
||||||
</Wizard.Stepper.Description>
|
|
||||||
</Wizard.Stepper.Step>
|
|
||||||
<Wizard.Stepper.Step of={Wizard.steps[3].id}>
|
|
||||||
<Wizard.Stepper.Title>4. Resumen</Wizard.Stepper.Title>
|
|
||||||
<Wizard.Stepper.Description>
|
|
||||||
Confirma y crea el plan
|
|
||||||
</Wizard.Stepper.Description>
|
|
||||||
</Wizard.Stepper.Step>
|
|
||||||
</Wizard.Stepper.Navigation>
|
|
||||||
|
|
||||||
{/* Info de paso actual */}
|
{/* Navegación dentro del bloque fijo */}
|
||||||
<div className="flex items-center justify-end px-1">
|
<div className="px-6 pb-6">
|
||||||
<span className="text-muted-foreground text-sm">
|
<Wizard.Stepper.Navigation className="border-border/60 rounded-xl border bg-slate-50 p-2">
|
||||||
Paso {Wizard.utils.getIndex(methods.current.id) + 1} de{' '}
|
{/* Nota: En móvil esto se verá apretado. Considera overflow-x-auto si tienes muchos pasos */}
|
||||||
{Wizard.steps.length}
|
<div className="flex items-center justify-between gap-2 overflow-x-auto">
|
||||||
</span>
|
{Wizard.steps.map((step) => (
|
||||||
|
<Wizard.Stepper.Step
|
||||||
|
key={step.id}
|
||||||
|
of={step.id}
|
||||||
|
className="whitespace-nowrap"
|
||||||
|
>
|
||||||
|
<Wizard.Stepper.Title>
|
||||||
|
{step.title}
|
||||||
|
</Wizard.Stepper.Title>
|
||||||
|
</Wizard.Stepper.Step>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Wizard.Stepper.Navigation>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Panel activo (solo uno visible) */}
|
{/* 2. CONTENIDO (SCROLLEABLE - Meat)
|
||||||
<div className="grid gap-6 md:grid-cols-2">
|
flex-1: Ocupa todo el espacio restante.
|
||||||
<div className="md:col-span-2">
|
overflow-y-auto: Scrollea solo esta parte.
|
||||||
{Wizard.utils.getIndex(methods.current.id) === 0 && (
|
*/}
|
||||||
<Wizard.Stepper.Panel>
|
<div className="flex-1 overflow-y-auto bg-gray-50/30 p-6">
|
||||||
<PasoModo wizard={wizard} onChange={setWizard} />
|
{/* Aquí renderizamos el panel. Quitamos el 'grid' del padre para evitar conflictos de altura */}
|
||||||
</Wizard.Stepper.Panel>
|
<div className="mx-auto max-w-3xl">
|
||||||
)}
|
{Wizard.utils.getIndex(methods.current.id) === 0 && (
|
||||||
{Wizard.utils.getIndex(methods.current.id) === 1 && (
|
<Wizard.Stepper.Panel>
|
||||||
<Wizard.Stepper.Panel>
|
<PasoModo wizard={wizard} onChange={setWizard} />
|
||||||
<PasoBasicos
|
</Wizard.Stepper.Panel>
|
||||||
wizard={wizard}
|
)}
|
||||||
onChange={setWizard}
|
{Wizard.utils.getIndex(methods.current.id) === 1 && (
|
||||||
carrerasFiltradas={carrerasFiltradas}
|
<Wizard.Stepper.Panel>
|
||||||
/>
|
<PasoBasicos
|
||||||
</Wizard.Stepper.Panel>
|
wizard={wizard}
|
||||||
)}
|
onChange={setWizard}
|
||||||
{Wizard.utils.getIndex(methods.current.id) === 2 && (
|
carrerasFiltradas={carrerasFiltradas}
|
||||||
<Wizard.Stepper.Panel>
|
/>
|
||||||
<PasoDetalles
|
</Wizard.Stepper.Panel>
|
||||||
wizard={wizard}
|
)}
|
||||||
onChange={setWizard}
|
{Wizard.utils.getIndex(methods.current.id) === 2 && (
|
||||||
onGenerarIA={generarPreviewIA}
|
<Wizard.Stepper.Panel>
|
||||||
isLoading={wizard.isLoading}
|
<PasoDetalles
|
||||||
/>
|
wizard={wizard}
|
||||||
</Wizard.Stepper.Panel>
|
onChange={setWizard}
|
||||||
)}
|
onGenerarIA={generarPreviewIA}
|
||||||
{Wizard.utils.getIndex(methods.current.id) === 3 && (
|
isLoading={wizard.isLoading}
|
||||||
<Wizard.Stepper.Panel>
|
/>
|
||||||
<PasoResumen wizard={wizard} />
|
</Wizard.Stepper.Panel>
|
||||||
</Wizard.Stepper.Panel>
|
)}
|
||||||
|
{Wizard.utils.getIndex(methods.current.id) === 3 && (
|
||||||
|
<Wizard.Stepper.Panel>
|
||||||
|
<PasoResumen wizard={wizard} />
|
||||||
|
</Wizard.Stepper.Panel>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 3. CONTROLES (FIJO - Bottom Bun)
|
||||||
|
flex-none: Siempre visible abajo.
|
||||||
|
*/}
|
||||||
|
<div className="flex-none border-t bg-white p-6">
|
||||||
|
<Wizard.Stepper.Controls className="flex items-center justify-between">
|
||||||
|
<div className="flex-1">
|
||||||
|
{wizard.errorMessage && (
|
||||||
|
<span className="text-destructive text-sm font-medium">
|
||||||
|
{wizard.errorMessage}
|
||||||
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Controles */}
|
<div className="flex gap-4">
|
||||||
<Wizard.Stepper.Controls>
|
|
||||||
{wizard.errorMessage && (
|
|
||||||
<span className="text-destructive mr-auto text-sm">
|
|
||||||
{wizard.errorMessage}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
onClick={() => methods.prev()}
|
|
||||||
disabled={
|
|
||||||
Wizard.utils.getIndex(methods.current.id) === 0 ||
|
|
||||||
wizard.isLoading
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Anterior
|
|
||||||
</Button>
|
|
||||||
{Wizard.utils.getIndex(methods.current.id) <
|
|
||||||
Wizard.steps.length - 1 ? (
|
|
||||||
<Button
|
<Button
|
||||||
onClick={() => methods.next()}
|
variant="secondary"
|
||||||
|
onClick={() => methods.prev()}
|
||||||
disabled={
|
disabled={
|
||||||
wizard.isLoading ||
|
Wizard.utils.getIndex(methods.current.id) === 0 ||
|
||||||
(Wizard.utils.getIndex(methods.current.id) === 0 &&
|
wizard.isLoading
|
||||||
!canContinueDesdeModo) ||
|
|
||||||
(Wizard.utils.getIndex(methods.current.id) === 1 &&
|
|
||||||
!canContinueDesdeBasicos) ||
|
|
||||||
(Wizard.utils.getIndex(methods.current.id) === 2 &&
|
|
||||||
!canContinueDesdeDetalles)
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
Siguiente
|
Anterior
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
|
||||||
<Button onClick={crearPlan} disabled={wizard.isLoading}>
|
{Wizard.utils.getIndex(methods.current.id) <
|
||||||
Crear plan
|
Wizard.steps.length - 1 ? (
|
||||||
</Button>
|
<Button
|
||||||
)}
|
onClick={() => methods.next()}
|
||||||
|
disabled={
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Siguiente
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button onClick={crearPlan} disabled={wizard.isLoading}>
|
||||||
|
Crear plan
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</Wizard.Stepper.Controls>
|
</Wizard.Stepper.Controls>
|
||||||
</>
|
</div>
|
||||||
)}
|
</>
|
||||||
</Wizard.Stepper.Provider>
|
)}
|
||||||
</div>
|
</Wizard.Stepper.Provider>
|
||||||
)}
|
)}
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
@@ -506,7 +515,7 @@ function PasoBasicos({
|
|||||||
carrerasFiltradas: typeof CARRERAS
|
carrerasFiltradas: typeof CARRERAS
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="grid gap-4 sm:grid-cols-2">
|
<div className="grid gap-20 sm:grid-cols-2">
|
||||||
<div className="sm:col-span-2">
|
<div className="sm:col-span-2">
|
||||||
<Label htmlFor="nombrePlan">Nombre del plan</Label>
|
<Label htmlFor="nombrePlan">Nombre del plan</Label>
|
||||||
<Input
|
<Input
|
||||||
|
|||||||
82
src/routes/stepper2.tsx
Normal file
82
src/routes/stepper2.tsx
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import { createFileRoute } from '@tanstack/react-router'
|
||||||
|
|
||||||
|
import { CircularProgress } from '@/components/CircularProgress'
|
||||||
|
import { defineStepper } from '@/components/stepper' // Tu wrapper
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
|
||||||
|
export const Route = createFileRoute('/stepper2')({
|
||||||
|
component: MobileStepperView,
|
||||||
|
})
|
||||||
|
|
||||||
|
// 1. Definimos los pasos igual que siempre
|
||||||
|
const myStepper = defineStepper(
|
||||||
|
{ id: 'contact', title: 'Contact Details' },
|
||||||
|
{ id: 'shipping', title: 'Shipping Information' },
|
||||||
|
{ id: 'billing', title: 'Billing Address' },
|
||||||
|
{ id: 'review', title: 'Payment Review' },
|
||||||
|
)
|
||||||
|
|
||||||
|
export default function MobileStepperView() {
|
||||||
|
return (
|
||||||
|
// Usa el Provider del wrapper para tener el contexto
|
||||||
|
<myStepper.Stepper.Provider>
|
||||||
|
{({ 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 (
|
||||||
|
<div className="flex h-full flex-col bg-white p-4">
|
||||||
|
{/* --- AQUÍ ESTÁ LA MAGIA (Tu UI Personalizada) --- */}
|
||||||
|
<div className="mb-6 flex items-center gap-4">
|
||||||
|
{/* El Gráfico Circular */}
|
||||||
|
<CircularProgress current={currentIndex} total={totalSteps} />
|
||||||
|
|
||||||
|
{/* Los Textos */}
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<h2 className="text-lg font-bold text-slate-900">
|
||||||
|
{methods.current.title}
|
||||||
|
</h2>
|
||||||
|
{nextStep && (
|
||||||
|
<p className="text-sm text-slate-400">
|
||||||
|
Next: {nextStep.title}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* ----------------------------------------------- */}
|
||||||
|
|
||||||
|
{/* El contenido de los pasos (Switch) */}
|
||||||
|
<div className="flex-1">
|
||||||
|
{methods.switch({
|
||||||
|
contact: () => <div>Formulario Contacto...</div>,
|
||||||
|
shipping: () => <div>Formulario Envío...</div>,
|
||||||
|
billing: () => <div>Formulario Facturación...</div>,
|
||||||
|
review: () => <div>Resumen...</div>,
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Controles de Navegación (Footer) */}
|
||||||
|
<div className="mt-4 flex justify-between">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
onClick={methods.prev}
|
||||||
|
disabled={methods.isFirst}
|
||||||
|
>
|
||||||
|
Back
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
className="bg-red-500 text-white hover:bg-red-600"
|
||||||
|
onClick={methods.next}
|
||||||
|
>
|
||||||
|
{methods.isLast ? 'Finish' : 'Next'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</myStepper.Stepper.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user