Separación vista/lógica del wizard de creación de plan
This commit is contained in:
@@ -1,42 +0,0 @@
|
|||||||
#root {
|
|
||||||
max-width: 1280px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 2rem;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo {
|
|
||||||
height: 6em;
|
|
||||||
padding: 1.5em;
|
|
||||||
will-change: filter;
|
|
||||||
transition: filter 300ms;
|
|
||||||
}
|
|
||||||
.logo:hover {
|
|
||||||
filter: drop-shadow(0 0 2em #646cffaa);
|
|
||||||
}
|
|
||||||
.logo.react:hover {
|
|
||||||
filter: drop-shadow(0 0 2em #61dafbaa);
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes logo-spin {
|
|
||||||
from {
|
|
||||||
transform: rotate(0deg);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-reduced-motion: no-preference) {
|
|
||||||
a:nth-of-type(2) .logo {
|
|
||||||
animation: logo-spin infinite 20s linear;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
padding: 2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.read-the-docs {
|
|
||||||
color: #888;
|
|
||||||
}
|
|
||||||
@@ -1,163 +0,0 @@
|
|||||||
import { defineStepper } from '@stepperize/react'
|
|
||||||
import * as React from 'react'
|
|
||||||
|
|
||||||
import { Button } from '@/components/ui/button'
|
|
||||||
import { Input } from '@/components/ui/input'
|
|
||||||
import { Separator } from '@/components/ui/separator'
|
|
||||||
import { Textarea } from '@/components/ui/textarea'
|
|
||||||
// import './App.css'
|
|
||||||
|
|
||||||
const { useStepper, steps, utils } = defineStepper(
|
|
||||||
{
|
|
||||||
id: 'shipping',
|
|
||||||
title: 'Shipping',
|
|
||||||
description: 'Enter your shipping details',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'payment',
|
|
||||||
title: 'Payment',
|
|
||||||
description: 'Enter your payment details',
|
|
||||||
},
|
|
||||||
{ id: 'complete', title: 'Complete', description: 'Checkout complete' },
|
|
||||||
)
|
|
||||||
|
|
||||||
function App() {
|
|
||||||
const stepper = useStepper()
|
|
||||||
|
|
||||||
const currentIndex = utils.getIndex(stepper.current.id)
|
|
||||||
return (
|
|
||||||
<div className="w-[450px] space-y-6 rounded-lg border p-6">
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<h2 className="text-lg font-medium">Checkout</h2>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className="text-muted-foreground text-sm">
|
|
||||||
Step {currentIndex + 1} of {steps.length}
|
|
||||||
</span>
|
|
||||||
<div />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<nav aria-label="Checkout Steps" className="group my-4">
|
|
||||||
<ol
|
|
||||||
className="flex items-center justify-between gap-2"
|
|
||||||
aria-orientation="horizontal"
|
|
||||||
>
|
|
||||||
{stepper.all.map((step, index, array) => (
|
|
||||||
<React.Fragment key={step.id}>
|
|
||||||
<li className="flex flex-shrink-0 items-center gap-4">
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
role="tab"
|
|
||||||
variant={index <= currentIndex ? 'default' : 'secondary'}
|
|
||||||
aria-current={
|
|
||||||
stepper.current.id === step.id ? 'step' : undefined
|
|
||||||
}
|
|
||||||
aria-posinset={index + 1}
|
|
||||||
aria-setsize={steps.length}
|
|
||||||
aria-selected={stepper.current.id === step.id}
|
|
||||||
className="flex size-10 items-center justify-center rounded-full"
|
|
||||||
onClick={() => stepper.goTo(step.id)}
|
|
||||||
>
|
|
||||||
{index + 1}
|
|
||||||
</Button>
|
|
||||||
<span className="text-sm font-medium">{step.title}</span>
|
|
||||||
</li>
|
|
||||||
{index < array.length - 1 && (
|
|
||||||
<Separator
|
|
||||||
className={`flex-1 ${
|
|
||||||
index < currentIndex ? 'bg-primary' : 'bg-muted'
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</React.Fragment>
|
|
||||||
))}
|
|
||||||
</ol>
|
|
||||||
</nav>
|
|
||||||
<div className="space-y-4">
|
|
||||||
{stepper.switch({
|
|
||||||
shipping: () => <ShippingComponent />,
|
|
||||||
payment: () => <PaymentComponent />,
|
|
||||||
complete: () => <CompleteComponent />,
|
|
||||||
})}
|
|
||||||
{!stepper.isLast ? (
|
|
||||||
<div className="flex justify-end gap-4">
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
onClick={stepper.prev}
|
|
||||||
disabled={stepper.isFirst}
|
|
||||||
>
|
|
||||||
Back
|
|
||||||
</Button>
|
|
||||||
<Button onClick={stepper.next}>
|
|
||||||
{stepper.isLast ? 'Complete' : 'Next'}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<Button onClick={stepper.reset}>Reset</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const ShippingComponent = () => {
|
|
||||||
return (
|
|
||||||
<div className="grid gap-4">
|
|
||||||
<div className="grid gap-2">
|
|
||||||
<label htmlFor="name" className="text-start text-sm font-medium">
|
|
||||||
Name
|
|
||||||
</label>
|
|
||||||
<Input id="name" placeholder="John Doe" className="w-full" />
|
|
||||||
</div>
|
|
||||||
<div className="grid gap-2">
|
|
||||||
<label htmlFor="address" className="text-start text-sm font-medium">
|
|
||||||
Address
|
|
||||||
</label>
|
|
||||||
<Textarea
|
|
||||||
id="address"
|
|
||||||
placeholder="123 Main St, Anytown USA"
|
|
||||||
className="w-full"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const PaymentComponent = () => {
|
|
||||||
return (
|
|
||||||
<div className="grid gap-4">
|
|
||||||
<div className="grid gap-2">
|
|
||||||
<label htmlFor="card-number" className="text-start text-sm font-medium">
|
|
||||||
Card Number
|
|
||||||
</label>
|
|
||||||
<Input
|
|
||||||
id="card-number"
|
|
||||||
placeholder="4111 1111 1111 1111"
|
|
||||||
className="w-full"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<div className="grid gap-2">
|
|
||||||
<label
|
|
||||||
htmlFor="expiry-date"
|
|
||||||
className="text-start text-sm font-medium"
|
|
||||||
>
|
|
||||||
Expiry Date
|
|
||||||
</label>
|
|
||||||
<Input id="expiry-date" placeholder="MM/YY" className="w-full" />
|
|
||||||
</div>
|
|
||||||
<div className="grid gap-2">
|
|
||||||
<label htmlFor="cvc" className="text-start text-sm font-medium">
|
|
||||||
CVC
|
|
||||||
</label>
|
|
||||||
<Input id="cvc" placeholder="123" className="w-full" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const CompleteComponent = () => {
|
|
||||||
return <h3 className="py-4 text-lg font-medium">Stepper complete 🔥</h3>
|
|
||||||
}
|
|
||||||
|
|
||||||
export default App
|
|
||||||
181
src/components/planes/wizard/PasoBasicosForm.tsx
Normal file
181
src/components/planes/wizard/PasoBasicosForm.tsx
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
import type { CARRERAS } from '@/features/planes/new/catalogs'
|
||||||
|
import type { NewPlanWizardState } from '@/features/planes/new/types'
|
||||||
|
|
||||||
|
import { Input } from '@/components/ui/input'
|
||||||
|
import { Label } from '@/components/ui/label'
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@/components/ui/select'
|
||||||
|
import {
|
||||||
|
FACULTADES,
|
||||||
|
NIVELES,
|
||||||
|
TIPOS_CICLO,
|
||||||
|
} from '@/features/planes/new/catalogs'
|
||||||
|
|
||||||
|
export function PasoBasicosForm({
|
||||||
|
wizard,
|
||||||
|
onChange,
|
||||||
|
carrerasFiltradas,
|
||||||
|
}: {
|
||||||
|
wizard: NewPlanWizardState
|
||||||
|
onChange: React.Dispatch<React.SetStateAction<NewPlanWizardState>>
|
||||||
|
carrerasFiltradas: typeof CARRERAS
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="grid gap-4 sm:grid-cols-2">
|
||||||
|
<div className="grid gap-1 sm:col-span-2">
|
||||||
|
<Label htmlFor="nombrePlan">Nombre del plan</Label>
|
||||||
|
<Input
|
||||||
|
id="nombrePlan"
|
||||||
|
placeholder="Ej. Ingeniería en Sistemas 2026"
|
||||||
|
value={wizard.datosBasicos.nombrePlan}
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
onChange((w) => ({
|
||||||
|
...w,
|
||||||
|
datosBasicos: { ...w.datosBasicos, nombrePlan: e.target.value },
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-1">
|
||||||
|
<Label htmlFor="facultad">Facultad</Label>
|
||||||
|
<Select
|
||||||
|
value={wizard.datosBasicos.facultadId}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
onChange((w) => ({
|
||||||
|
...w,
|
||||||
|
datosBasicos: {
|
||||||
|
...w.datosBasicos,
|
||||||
|
facultadId: value,
|
||||||
|
carreraId: '',
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger
|
||||||
|
id="facultad"
|
||||||
|
className="w-full min-w-0 [&>span]:block! [&>span]:truncate!"
|
||||||
|
>
|
||||||
|
<SelectValue placeholder="Selecciona facultad…" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{FACULTADES.map((f) => (
|
||||||
|
<SelectItem key={f.id} value={f.id}>
|
||||||
|
{f.nombre}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-1">
|
||||||
|
<Label htmlFor="carrera">Carrera</Label>
|
||||||
|
<Select
|
||||||
|
value={wizard.datosBasicos.carreraId}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
onChange((w) => ({
|
||||||
|
...w,
|
||||||
|
datosBasicos: { ...w.datosBasicos, carreraId: value },
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
disabled={!wizard.datosBasicos.facultadId}
|
||||||
|
>
|
||||||
|
<SelectTrigger
|
||||||
|
id="carrera"
|
||||||
|
className="w-full min-w-0 [&>span]:block! [&>span]:truncate!"
|
||||||
|
>
|
||||||
|
<SelectValue placeholder="Selecciona carrera…" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{carrerasFiltradas.map((c) => (
|
||||||
|
<SelectItem key={c.id} value={c.id}>
|
||||||
|
{c.nombre}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-1">
|
||||||
|
<Label htmlFor="nivel">Nivel</Label>
|
||||||
|
<Select
|
||||||
|
value={wizard.datosBasicos.nivel}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
onChange((w) => ({
|
||||||
|
...w,
|
||||||
|
datosBasicos: { ...w.datosBasicos, nivel: value },
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger
|
||||||
|
id="nivel"
|
||||||
|
className="w-full min-w-0 [&>span]:block! [&>span]:truncate!"
|
||||||
|
>
|
||||||
|
<SelectValue placeholder="Selecciona nivel…" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{NIVELES.map((n) => (
|
||||||
|
<SelectItem key={n} value={n}>
|
||||||
|
{n}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-1">
|
||||||
|
<Label htmlFor="tipoCiclo">Tipo de ciclo</Label>
|
||||||
|
<Select
|
||||||
|
value={wizard.datosBasicos.tipoCiclo}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
onChange((w) => ({
|
||||||
|
...w,
|
||||||
|
datosBasicos: {
|
||||||
|
...w.datosBasicos,
|
||||||
|
tipoCiclo: value as any,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger
|
||||||
|
id="tipoCiclo"
|
||||||
|
className="w-full min-w-0 [&>span]:block! [&>span]:truncate!"
|
||||||
|
>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{TIPOS_CICLO.map((t) => (
|
||||||
|
<SelectItem key={t.value} value={t.value}>
|
||||||
|
{t.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-1">
|
||||||
|
<Label htmlFor="numCiclos">Número de ciclos</Label>
|
||||||
|
<Input
|
||||||
|
id="numCiclos"
|
||||||
|
type="number"
|
||||||
|
min={1}
|
||||||
|
value={wizard.datosBasicos.numCiclos}
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
onChange((w) => ({
|
||||||
|
...w,
|
||||||
|
datosBasicos: {
|
||||||
|
...w.datosBasicos,
|
||||||
|
numCiclos: Number(e.target.value || 1),
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
322
src/components/planes/wizard/PasoDetallesPanel.tsx
Normal file
322
src/components/planes/wizard/PasoDetallesPanel.tsx
Normal file
@@ -0,0 +1,322 @@
|
|||||||
|
import type { NewPlanWizardState } from '@/features/planes/new/types'
|
||||||
|
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from '@/components/ui/card'
|
||||||
|
import { Input } from '@/components/ui/input'
|
||||||
|
import { Label } from '@/components/ui/label'
|
||||||
|
import {
|
||||||
|
CARRERAS,
|
||||||
|
FACULTADES,
|
||||||
|
PLANES_EXISTENTES,
|
||||||
|
} from '@/features/planes/new/catalogs'
|
||||||
|
|
||||||
|
export function PasoDetallesPanel({
|
||||||
|
wizard,
|
||||||
|
onChange,
|
||||||
|
onGenerarIA,
|
||||||
|
isLoading,
|
||||||
|
}: {
|
||||||
|
wizard: NewPlanWizardState
|
||||||
|
onChange: React.Dispatch<React.SetStateAction<NewPlanWizardState>>
|
||||||
|
onGenerarIA: () => void
|
||||||
|
isLoading: boolean
|
||||||
|
}) {
|
||||||
|
if (wizard.modoCreacion === 'MANUAL') {
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Creación manual</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Se creará un plan en blanco con estructura mínima.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wizard.modoCreacion === 'IA') {
|
||||||
|
return (
|
||||||
|
<div className="grid gap-4">
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="desc">Descripción del enfoque</Label>
|
||||||
|
<textarea
|
||||||
|
id="desc"
|
||||||
|
className="bg-background text-foreground ring-offset-background focus-visible:ring-ring min-h-24 w-full rounded-md border px-3 py-2 text-sm shadow-sm focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none"
|
||||||
|
placeholder="Describe el enfoque del programa…"
|
||||||
|
value={wizard.iaConfig?.descripcionEnfoque || ''}
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) =>
|
||||||
|
onChange((w) => ({
|
||||||
|
...w,
|
||||||
|
iaConfig: {
|
||||||
|
...(w.iaConfig || ({} as any)),
|
||||||
|
descripcionEnfoque: e.target.value,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="poblacion">Población objetivo</Label>
|
||||||
|
<Input
|
||||||
|
id="poblacion"
|
||||||
|
placeholder="Ej. Egresados de bachillerato con perfil STEM"
|
||||||
|
value={wizard.iaConfig?.poblacionObjetivo || ''}
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
onChange((w) => ({
|
||||||
|
...w,
|
||||||
|
iaConfig: {
|
||||||
|
...(w.iaConfig || ({} as any)),
|
||||||
|
poblacionObjetivo: e.target.value,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="notas">Notas adicionales</Label>
|
||||||
|
<textarea
|
||||||
|
id="notas"
|
||||||
|
className="bg-background text-foreground ring-offset-background focus-visible:ring-ring min-h-24 w-full rounded-md border px-3 py-2 text-sm shadow-sm focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none"
|
||||||
|
placeholder="Lineamientos institucionales, restricciones, etc."
|
||||||
|
value={wizard.iaConfig?.notasAdicionales || ''}
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) =>
|
||||||
|
onChange((w) => ({
|
||||||
|
...w,
|
||||||
|
iaConfig: {
|
||||||
|
...(w.iaConfig || ({} as any)),
|
||||||
|
notasAdicionales: e.target.value,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="text-muted-foreground text-sm">
|
||||||
|
Opcional: se pueden adjuntar recursos IA más adelante.
|
||||||
|
</div>
|
||||||
|
<Button onClick={onGenerarIA} disabled={isLoading}>
|
||||||
|
{isLoading ? 'Generando…' : 'Generar borrador con IA'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{wizard.resumen.previewPlan && (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Preview IA</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Materias aprox.: {wizard.resumen.previewPlan.numMateriasAprox}
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<ul className="text-muted-foreground list-disc pl-5 text-sm">
|
||||||
|
{wizard.resumen.previewPlan.secciones?.map((s) => (
|
||||||
|
<li key={s.id}>
|
||||||
|
<span className="text-foreground font-medium">
|
||||||
|
{s.titulo}:
|
||||||
|
</span>{' '}
|
||||||
|
{s.resumen}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
wizard.modoCreacion === 'CLONADO' &&
|
||||||
|
wizard.subModoClonado === 'INTERNO'
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<div className="grid gap-4">
|
||||||
|
<div className="grid gap-3 sm:grid-cols-3">
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="clonFacultad">Facultad</Label>
|
||||||
|
<select
|
||||||
|
id="clonFacultad"
|
||||||
|
className="bg-background text-foreground ring-offset-background focus-visible:ring-ring h-10 w-full rounded-md border px-3 text-sm shadow-sm focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none"
|
||||||
|
value={wizard.datosBasicos.facultadId}
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLSelectElement>) =>
|
||||||
|
onChange((w) => ({
|
||||||
|
...w,
|
||||||
|
datosBasicos: {
|
||||||
|
...w.datosBasicos,
|
||||||
|
facultadId: e.target.value,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<option value="">Todas</option>
|
||||||
|
{FACULTADES.map((f) => (
|
||||||
|
<option key={f.id} value={f.id}>
|
||||||
|
{f.nombre}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="clonCarrera">Carrera</Label>
|
||||||
|
<select
|
||||||
|
id="clonCarrera"
|
||||||
|
className="bg-background text-foreground ring-offset-background focus-visible:ring-ring h-10 w-full rounded-md border px-3 text-sm shadow-sm focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none"
|
||||||
|
value={wizard.datosBasicos.carreraId}
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLSelectElement>) =>
|
||||||
|
onChange((w) => ({
|
||||||
|
...w,
|
||||||
|
datosBasicos: {
|
||||||
|
...w.datosBasicos,
|
||||||
|
carreraId: e.target.value,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<option value="">Todas</option>
|
||||||
|
{CARRERAS.map((c) => (
|
||||||
|
<option key={c.id} value={c.id}>
|
||||||
|
{c.nombre}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="buscarPlan">Buscar</Label>
|
||||||
|
<Input
|
||||||
|
id="buscarPlan"
|
||||||
|
placeholder="Nombre del plan…"
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const term = e.target.value.toLowerCase()
|
||||||
|
void term
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-3">
|
||||||
|
{PLANES_EXISTENTES.filter(
|
||||||
|
(p) =>
|
||||||
|
(!wizard.datosBasicos.facultadId ||
|
||||||
|
p.facultadId === wizard.datosBasicos.facultadId) &&
|
||||||
|
(!wizard.datosBasicos.carreraId ||
|
||||||
|
p.carreraId === wizard.datosBasicos.carreraId),
|
||||||
|
).map((p) => (
|
||||||
|
<Card
|
||||||
|
key={p.id}
|
||||||
|
className={
|
||||||
|
p.id === wizard.clonInterno?.planOrigenId
|
||||||
|
? 'ring-ring ring-2'
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
onClick={() =>
|
||||||
|
onChange((w) => ({ ...w, clonInterno: { planOrigenId: p.id } }))
|
||||||
|
}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center justify-between">
|
||||||
|
<span>{p.nombre}</span>
|
||||||
|
<span className="text-muted-foreground text-sm">
|
||||||
|
{p.estado} · {p.anio}
|
||||||
|
</span>
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>ID: {p.id}</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
wizard.modoCreacion === 'CLONADO' &&
|
||||||
|
wizard.subModoClonado === 'TRADICIONAL'
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<div className="grid gap-4">
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="word">Word del plan (obligatorio)</Label>
|
||||||
|
<input
|
||||||
|
id="word"
|
||||||
|
type="file"
|
||||||
|
accept=".doc,.docx"
|
||||||
|
className="bg-background text-foreground ring-offset-background focus-visible:ring-ring file:bg-secondary block w-full rounded-md border px-3 py-2 text-sm shadow-sm file:mr-4 file:rounded-md file:border-0 file:px-3 file:py-1.5 file:text-sm focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none"
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
onChange((w) => ({
|
||||||
|
...w,
|
||||||
|
clonTradicional: {
|
||||||
|
...(w.clonTradicional || ({} as any)),
|
||||||
|
archivoWordPlanId: e.target.files?.[0]
|
||||||
|
? `file_${e.target.files[0].name}`
|
||||||
|
: null,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="mapa">Excel del mapa curricular</Label>
|
||||||
|
<input
|
||||||
|
id="mapa"
|
||||||
|
type="file"
|
||||||
|
accept=".xls,.xlsx"
|
||||||
|
className="bg-background text-foreground ring-offset-background focus-visible:ring-ring file:bg-secondary block w-full rounded-md border px-3 py-2 text-sm shadow-sm file:mr-4 file:rounded-md file:border-0 file:px-3 file:py-1.5 file:text-sm focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none"
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
onChange((w) => ({
|
||||||
|
...w,
|
||||||
|
clonTradicional: {
|
||||||
|
...(w.clonTradicional || ({} as any)),
|
||||||
|
archivoMapaExcelId: e.target.files?.[0]
|
||||||
|
? `file_${e.target.files[0].name}`
|
||||||
|
: null,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="materias">Excel/listado de materias</Label>
|
||||||
|
<input
|
||||||
|
id="materias"
|
||||||
|
type="file"
|
||||||
|
accept=".xls,.xlsx,.csv"
|
||||||
|
className="bg-background text-foreground ring-offset-background focus-visible:ring-ring file:bg-secondary block w-full rounded-md border px-3 py-2 text-sm shadow-sm file:mr-4 file:rounded-md file:border-0 file:px-3 file:py-1.5 file:text-sm focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none"
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
onChange((w) => ({
|
||||||
|
...w,
|
||||||
|
clonTradicional: {
|
||||||
|
...(w.clonTradicional || ({} as any)),
|
||||||
|
archivoMateriasExcelId: e.target.files?.[0]
|
||||||
|
? `file_${e.target.files[0].name}`
|
||||||
|
: null,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="text-muted-foreground text-sm">
|
||||||
|
Sube al menos Word y uno de los Excel para continuar.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Selecciona un modo</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Elige una opción en el paso anterior para continuar.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
122
src/components/planes/wizard/PasoModoCardGroup.tsx
Normal file
122
src/components/planes/wizard/PasoModoCardGroup.tsx
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
import * as Icons from 'lucide-react'
|
||||||
|
|
||||||
|
import type {
|
||||||
|
NewPlanWizardState,
|
||||||
|
ModoCreacion,
|
||||||
|
SubModoClonado,
|
||||||
|
} from '@/features/planes/new/types'
|
||||||
|
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from '@/components/ui/card'
|
||||||
|
|
||||||
|
export function PasoModoCardGroup({
|
||||||
|
wizard,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
wizard: NewPlanWizardState
|
||||||
|
onChange: React.Dispatch<React.SetStateAction<NewPlanWizardState>>
|
||||||
|
}) {
|
||||||
|
const isSelected = (m: ModoCreacion) => wizard.modoCreacion === m
|
||||||
|
const isSubSelected = (s: SubModoClonado) => wizard.subModoClonado === s
|
||||||
|
return (
|
||||||
|
<div className="grid gap-4 sm:grid-cols-3">
|
||||||
|
<Card
|
||||||
|
className={isSelected('MANUAL') ? 'ring-ring ring-2' : ''}
|
||||||
|
onClick={() =>
|
||||||
|
onChange((w) => ({
|
||||||
|
...w,
|
||||||
|
modoCreacion: 'MANUAL',
|
||||||
|
subModoClonado: undefined,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<Icons.Pencil className="text-primary h-5 w-5" /> Manual
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>Plan vacío con estructura mínima.</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card
|
||||||
|
className={isSelected('IA') ? 'ring-ring ring-2' : ''}
|
||||||
|
onClick={() =>
|
||||||
|
onChange((w) => ({
|
||||||
|
...w,
|
||||||
|
modoCreacion: 'IA',
|
||||||
|
subModoClonado: undefined,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<Icons.Sparkles className="text-primary h-5 w-5" /> Con IA
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Borrador completo a partir de datos base.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card
|
||||||
|
className={isSelected('CLONADO') ? 'ring-ring ring-2' : ''}
|
||||||
|
onClick={() => onChange((w) => ({ ...w, modoCreacion: 'CLONADO' }))}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<Icons.Copy className="text-primary h-5 w-5" /> Clonado
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>Desde un plan existente o archivos.</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
{wizard.modoCreacion === 'CLONADO' && (
|
||||||
|
<CardContent className="flex flex-col gap-3">
|
||||||
|
<div
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
onChange((w) => ({ ...w, subModoClonado: 'INTERNO' }))
|
||||||
|
}}
|
||||||
|
className={`hover:border-primary/50 hover:bg-accent flex cursor-pointer flex-row items-center justify-center gap-2 rounded-lg border p-4 text-center transition-all sm:flex-col ${
|
||||||
|
isSubSelected('INTERNO')
|
||||||
|
? 'border-primary bg-primary/5 ring-primary text-primary ring-1'
|
||||||
|
: 'border-border text-muted-foreground'
|
||||||
|
} `}
|
||||||
|
>
|
||||||
|
<Icons.Database className="mb-1 h-6 w-6" />
|
||||||
|
<span className="text-sm font-medium">Del sistema</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
onChange((w) => ({ ...w, subModoClonado: 'TRADICIONAL' }))
|
||||||
|
}}
|
||||||
|
className={`hover:border-primary/50 hover:bg-accent flex cursor-pointer flex-row items-center justify-center gap-2 rounded-lg border p-4 text-center transition-all sm:flex-col ${
|
||||||
|
isSubSelected('TRADICIONAL')
|
||||||
|
? 'border-primary bg-primary/5 ring-primary text-primary ring-1'
|
||||||
|
: 'border-border text-muted-foreground'
|
||||||
|
} `}
|
||||||
|
>
|
||||||
|
<Icons.Upload className="mb-1 h-6 w-6" />
|
||||||
|
<span className="text-sm font-medium">Desde archivos</span>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
74
src/components/planes/wizard/PasoResumenCard.tsx
Normal file
74
src/components/planes/wizard/PasoResumenCard.tsx
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
import type { NewPlanWizardState } from '@/features/planes/new/types'
|
||||||
|
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from '@/components/ui/card'
|
||||||
|
|
||||||
|
export function PasoResumenCard({ wizard }: { wizard: NewPlanWizardState }) {
|
||||||
|
const modo = wizard.modoCreacion
|
||||||
|
const sub = wizard.subModoClonado
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Resumen</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Verifica la información antes de crear.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="grid gap-2 text-sm">
|
||||||
|
<div>
|
||||||
|
<span className="text-muted-foreground">Nombre: </span>
|
||||||
|
<span className="font-medium">
|
||||||
|
{wizard.datosBasicos.nombrePlan || '—'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-muted-foreground">Facultad/Carrera: </span>
|
||||||
|
<span className="font-medium">
|
||||||
|
{wizard.datosBasicos.facultadId || '—'} /{' '}
|
||||||
|
{wizard.datosBasicos.carreraId || '—'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-muted-foreground">Nivel: </span>
|
||||||
|
<span className="font-medium">
|
||||||
|
{wizard.datosBasicos.nivel || '—'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-muted-foreground">Ciclos: </span>
|
||||||
|
<span className="font-medium">
|
||||||
|
{wizard.datosBasicos.numCiclos} ({wizard.datosBasicos.tipoCiclo})
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="mt-2">
|
||||||
|
<span className="text-muted-foreground">Modo: </span>
|
||||||
|
<span className="font-medium">
|
||||||
|
{modo === 'MANUAL' && 'Manual'}
|
||||||
|
{modo === 'IA' && 'Generado con IA'}
|
||||||
|
{modo === 'CLONADO' &&
|
||||||
|
sub === 'INTERNO' &&
|
||||||
|
'Clonado desde plan del sistema'}
|
||||||
|
{modo === 'CLONADO' &&
|
||||||
|
sub === 'TRADICIONAL' &&
|
||||||
|
'Importado desde documentos tradicionales'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{wizard.resumen.previewPlan && (
|
||||||
|
<div className="bg-muted mt-2 rounded-md p-3">
|
||||||
|
<div className="font-medium">Preview IA</div>
|
||||||
|
<div className="text-muted-foreground">
|
||||||
|
Materias aprox.: {wizard.resumen.previewPlan.numMateriasAprox}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
41
src/components/planes/wizard/StepWithTooltip.tsx
Normal file
41
src/components/planes/wizard/StepWithTooltip.tsx
Normal file
@@ -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 (
|
||||||
|
<TooltipProvider delayDuration={0}>
|
||||||
|
<Tooltip open={isOpen} onOpenChange={setIsOpen}>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<span
|
||||||
|
className="cursor-help decoration-dotted underline-offset-4 hover:underline"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
setIsOpen((prev) => !prev)
|
||||||
|
}}
|
||||||
|
onMouseEnter={() => setIsOpen(true)}
|
||||||
|
onMouseLeave={() => setIsOpen(false)}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</span>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent className="max-w-[200px] text-xs">
|
||||||
|
<p>{desc}</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
47
src/components/planes/wizard/WizardControls.tsx
Normal file
47
src/components/planes/wizard/WizardControls.tsx
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
|
||||||
|
export function WizardControls({
|
||||||
|
errorMessage,
|
||||||
|
onPrev,
|
||||||
|
onNext,
|
||||||
|
onCreate,
|
||||||
|
disablePrev,
|
||||||
|
disableNext,
|
||||||
|
disableCreate,
|
||||||
|
isLastStep,
|
||||||
|
}: {
|
||||||
|
errorMessage?: string | null
|
||||||
|
onPrev: () => void
|
||||||
|
onNext: () => void
|
||||||
|
onCreate: () => void
|
||||||
|
disablePrev: boolean
|
||||||
|
disableNext: boolean
|
||||||
|
disableCreate: boolean
|
||||||
|
isLastStep: boolean
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex-1">
|
||||||
|
{errorMessage && (
|
||||||
|
<span className="text-destructive text-sm font-medium">
|
||||||
|
{errorMessage}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<Button variant="secondary" onClick={onPrev} disabled={disablePrev}>
|
||||||
|
Anterior
|
||||||
|
</Button>
|
||||||
|
{isLastStep ? (
|
||||||
|
<Button onClick={onCreate} disabled={disableCreate}>
|
||||||
|
Crear plan
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button onClick={onNext} disabled={disableNext}>
|
||||||
|
Siguiente
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
78
src/components/planes/wizard/WizardHeader.tsx
Normal file
78
src/components/planes/wizard/WizardHeader.tsx
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import * as Icons from 'lucide-react'
|
||||||
|
|
||||||
|
import { StepWithTooltip } from './StepWithTooltip'
|
||||||
|
|
||||||
|
import { CircularProgress } from '@/components/CircularProgress'
|
||||||
|
import { DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||||
|
|
||||||
|
export function WizardHeader({
|
||||||
|
currentIndex,
|
||||||
|
totalSteps,
|
||||||
|
currentTitle,
|
||||||
|
currentDescription,
|
||||||
|
nextTitle,
|
||||||
|
onClose,
|
||||||
|
Wizard,
|
||||||
|
}: {
|
||||||
|
currentIndex: number
|
||||||
|
totalSteps: number
|
||||||
|
currentTitle: string
|
||||||
|
currentDescription: string
|
||||||
|
nextTitle?: string
|
||||||
|
onClose: () => void
|
||||||
|
Wizard: any
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="z-10 flex-none border-b bg-white">
|
||||||
|
<div className="flex items-center justify-between p-6 pb-4">
|
||||||
|
<DialogHeader className="p-0">
|
||||||
|
<DialogTitle>Nuevo plan de estudios</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-none disabled:pointer-events-none"
|
||||||
|
>
|
||||||
|
<Icons.X className="h-4 w-4" />
|
||||||
|
<span className="sr-only">Cerrar</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="px-6 pb-6">
|
||||||
|
<div className="block sm:hidden">
|
||||||
|
<div className="flex items-center gap-5">
|
||||||
|
<CircularProgress current={currentIndex} total={totalSteps} />
|
||||||
|
<div className="flex flex-col justify-center">
|
||||||
|
<h2 className="text-lg font-bold text-slate-900">
|
||||||
|
<StepWithTooltip
|
||||||
|
title={currentTitle}
|
||||||
|
desc={currentDescription}
|
||||||
|
/>
|
||||||
|
</h2>
|
||||||
|
{nextTitle ? (
|
||||||
|
<p className="text-sm text-slate-400">Siguiente: {nextTitle}</p>
|
||||||
|
) : (
|
||||||
|
<p className="text-sm font-medium text-green-500">
|
||||||
|
¡Último paso!
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="hidden sm:block">
|
||||||
|
<Wizard.Stepper.Navigation className="border-border/60 rounded-xl border bg-slate-50 p-2">
|
||||||
|
{Wizard.steps.map((step: any) => (
|
||||||
|
<Wizard.Stepper.Step
|
||||||
|
key={step.id}
|
||||||
|
of={step.id}
|
||||||
|
className="whitespace-nowrap"
|
||||||
|
>
|
||||||
|
<Wizard.Stepper.Title>
|
||||||
|
<StepWithTooltip title={step.title} desc={step.description} />
|
||||||
|
</Wizard.Stepper.Title>
|
||||||
|
</Wizard.Stepper.Step>
|
||||||
|
))}
|
||||||
|
</Wizard.Stepper.Navigation>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -57,15 +57,17 @@ const defineStepper = <const Steps extends Array<Stepperize.Step>>(
|
|||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
|
// Avoid leaking non-DOM props like `initialStep` onto the div
|
||||||
|
const { initialStep, initialMetadata, ...restProps } = props as {
|
||||||
|
initialStep?: any
|
||||||
|
initialMetadata?: any
|
||||||
|
} & Record<string, unknown>
|
||||||
return (
|
return (
|
||||||
<StepperContext.Provider
|
<StepperContext.Provider
|
||||||
value={{ variant, labelOrientation, tracking }}
|
value={{ variant, labelOrientation, tracking }}
|
||||||
>
|
>
|
||||||
<Scoped
|
<Scoped initialStep={initialStep} initialMetadata={initialMetadata}>
|
||||||
initialStep={props.initialStep}
|
<StepperContainer className={className} {...(restProps as any)}>
|
||||||
initialMetadata={props.initialMetadata}
|
|
||||||
>
|
|
||||||
<StepperContainer className={className} {...props}>
|
|
||||||
{children}
|
{children}
|
||||||
</StepperContainer>
|
</StepperContainer>
|
||||||
</Scoped>
|
</Scoped>
|
||||||
|
|||||||
205
src/features/planes/new/NuevoPlanModalContainer.tsx
Normal file
205
src/features/planes/new/NuevoPlanModalContainer.tsx
Normal file
@@ -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 (
|
||||||
|
<Dialog open={true} onOpenChange={(open) => !open && handleClose()}>
|
||||||
|
<DialogContent
|
||||||
|
className="flex h-[90vh] w-[calc(100%-2rem)] flex-col gap-0 overflow-hidden p-0 sm:max-w-4xl"
|
||||||
|
onInteractOutside={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{role !== 'JEFE_CARRERA' ? (
|
||||||
|
<>
|
||||||
|
<DialogHeader className="flex-none border-b p-6">
|
||||||
|
<DialogTitle>Nuevo plan de estudios</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="flex-1 p-6">
|
||||||
|
<Card className="border-destructive/40">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<Icons.ShieldAlert className="text-destructive h-5 w-5" />
|
||||||
|
Sin permisos
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
No tienes permisos para crear planes de estudio.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="flex justify-end">
|
||||||
|
<button
|
||||||
|
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground rounded-md border px-3 py-2 text-sm opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-none"
|
||||||
|
onClick={handleClose}
|
||||||
|
>
|
||||||
|
Volver
|
||||||
|
</button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<Wizard.Stepper.Provider
|
||||||
|
initialStep={Wizard.utils.getFirst().id}
|
||||||
|
className="flex h-full flex-col"
|
||||||
|
>
|
||||||
|
{({ methods }) => {
|
||||||
|
const currentIndex = Wizard.utils.getIndex(methods.current.id) + 1
|
||||||
|
const totalSteps = Wizard.steps.length
|
||||||
|
const nextStep = Wizard.steps[currentIndex]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<WizardHeader
|
||||||
|
currentIndex={currentIndex}
|
||||||
|
totalSteps={totalSteps}
|
||||||
|
currentTitle={methods.current.title}
|
||||||
|
currentDescription={methods.current.description}
|
||||||
|
nextTitle={nextStep?.title}
|
||||||
|
onClose={handleClose}
|
||||||
|
Wizard={Wizard}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex-1 overflow-y-auto bg-gray-50/30 p-6">
|
||||||
|
<div className="mx-auto max-w-3xl">
|
||||||
|
{Wizard.utils.getIndex(methods.current.id) === 0 && (
|
||||||
|
<Wizard.Stepper.Panel>
|
||||||
|
<PasoModoCardGroup
|
||||||
|
wizard={wizard}
|
||||||
|
onChange={setWizard}
|
||||||
|
/>
|
||||||
|
</Wizard.Stepper.Panel>
|
||||||
|
)}
|
||||||
|
{Wizard.utils.getIndex(methods.current.id) === 1 && (
|
||||||
|
<Wizard.Stepper.Panel>
|
||||||
|
<PasoBasicosForm
|
||||||
|
wizard={wizard}
|
||||||
|
onChange={setWizard}
|
||||||
|
carrerasFiltradas={carrerasFiltradas}
|
||||||
|
/>
|
||||||
|
</Wizard.Stepper.Panel>
|
||||||
|
)}
|
||||||
|
{Wizard.utils.getIndex(methods.current.id) === 2 && (
|
||||||
|
<Wizard.Stepper.Panel>
|
||||||
|
<PasoDetallesPanel
|
||||||
|
wizard={wizard}
|
||||||
|
onChange={setWizard}
|
||||||
|
onGenerarIA={generarPreviewIA}
|
||||||
|
isLoading={wizard.isLoading}
|
||||||
|
/>
|
||||||
|
</Wizard.Stepper.Panel>
|
||||||
|
)}
|
||||||
|
{Wizard.utils.getIndex(methods.current.id) === 3 && (
|
||||||
|
<Wizard.Stepper.Panel>
|
||||||
|
<PasoResumenCard wizard={wizard} />
|
||||||
|
</Wizard.Stepper.Panel>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-none border-t bg-white p-6">
|
||||||
|
<Wizard.Stepper.Controls>
|
||||||
|
<WizardControls
|
||||||
|
errorMessage={wizard.errorMessage}
|
||||||
|
onPrev={() => 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
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Wizard.Stepper.Controls>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</Wizard.Stepper.Provider>
|
||||||
|
)}
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
56
src/features/planes/new/catalogs.ts
Normal file
56
src/features/planes/new/catalogs.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
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",
|
||||||
|
},
|
||||||
|
];
|
||||||
102
src/features/planes/new/hooks/useNuevoPlanWizard.ts
Normal file
102
src/features/planes/new/hooks/useNuevoPlanWizard.ts
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
import { useMemo, useState } from "react";
|
||||||
|
|
||||||
|
import { CARRERAS } from "../catalogs";
|
||||||
|
|
||||||
|
import type { NewPlanWizardState, PlanPreview } from "../types";
|
||||||
|
|
||||||
|
export function useNuevoPlanWizard() {
|
||||||
|
const [wizard, setWizard] = useState<NewPlanWizardState>({
|
||||||
|
step: 1,
|
||||||
|
modoCreacion: null,
|
||||||
|
datosBasicos: {
|
||||||
|
nombrePlan: "",
|
||||||
|
carreraId: "",
|
||||||
|
facultadId: "",
|
||||||
|
nivel: "",
|
||||||
|
tipoCiclo: "SEMESTRE",
|
||||||
|
numCiclos: 8,
|
||||||
|
},
|
||||||
|
clonInterno: { planOrigenId: null },
|
||||||
|
clonTradicional: {
|
||||||
|
archivoWordPlanId: null,
|
||||||
|
archivoMapaExcelId: null,
|
||||||
|
archivoMateriasExcelId: 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 > 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.archivoMateriasExcelId;
|
||||||
|
return tieneWord && tieneAlMenosUnExcel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
})();
|
||||||
|
|
||||||
|
const generarPreviewIA = async () => {
|
||||||
|
setWizard((w) => ({ ...w, isLoading: true, errorMessage: null }));
|
||||||
|
await new Promise((r) => setTimeout(r, 800));
|
||||||
|
const preview: PlanPreview = {
|
||||||
|
nombrePlan: wizard.datosBasicos.nombrePlan || "Plan sin nombre",
|
||||||
|
nivel: wizard.datosBasicos.nivel || "Licenciatura",
|
||||||
|
tipoCiclo: wizard.datosBasicos.tipoCiclo,
|
||||||
|
numCiclos: wizard.datosBasicos.numCiclos,
|
||||||
|
numMateriasAprox: wizard.datosBasicos.numCiclos * 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
41
src/features/planes/new/types.ts
Normal file
41
src/features/planes/new/types.ts
Normal file
@@ -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;
|
||||||
|
numMateriasAprox?: 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;
|
||||||
|
};
|
||||||
|
clonInterno?: { planOrigenId: string | null };
|
||||||
|
clonTradicional?: {
|
||||||
|
archivoWordPlanId: string | null;
|
||||||
|
archivoMapaExcelId: string | null;
|
||||||
|
archivoMateriasExcelId: string | null;
|
||||||
|
};
|
||||||
|
iaConfig?: {
|
||||||
|
descripcionEnfoque: string;
|
||||||
|
poblacionObjetivo: string;
|
||||||
|
notasAdicionales: string;
|
||||||
|
archivosReferencia: Array<string>;
|
||||||
|
};
|
||||||
|
resumen: { previewPlan?: PlanPreview };
|
||||||
|
isLoading: boolean;
|
||||||
|
errorMessage: string | null;
|
||||||
|
};
|
||||||
@@ -9,29 +9,17 @@
|
|||||||
// 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 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'
|
||||||
import { Route as IndexRouteImport } from './routes/index'
|
import { Route as IndexRouteImport } from './routes/index'
|
||||||
import { Route as DemoTanstackQueryRouteImport } from './routes/demo/tanstack-query'
|
import { Route as DemoTanstackQueryRouteImport } from './routes/demo/tanstack-query'
|
||||||
import { Route as PlanesListaRouteRouteImport } from './routes/planes/_lista/route'
|
import { Route as PlanesListaRouteRouteImport } from './routes/planes/_lista/route'
|
||||||
import { Route as PlanesPlanIdRouteRouteImport } from './routes/planes/$planId/route'
|
import { Route as PlanesPlanIdIndexRouteImport } from './routes/planes/$planId/index'
|
||||||
import { Route as AsignaturasListaRouteRouteImport } from './routes/asignaturas/_lista/route'
|
|
||||||
import { Route as AsignaturasAsignaturaIdRouteRouteImport } from './routes/asignaturas/$asignaturaId/route'
|
|
||||||
import { Route as PlanesListaNuevoRouteImport } from './routes/planes/_lista/nuevo'
|
import { Route as PlanesListaNuevoRouteImport } from './routes/planes/_lista/nuevo'
|
||||||
import { Route as AsignaturasListaNuevaRouteImport } from './routes/asignaturas/_lista/nueva'
|
import { Route as PlanesPlanIdAsignaturasListaRouteRouteImport } from './routes/planes/$planId/asignaturas/_lista/route'
|
||||||
|
import { Route as PlanesPlanIdAsignaturasAsignaturaIdRouteRouteImport } from './routes/planes/$planId/asignaturas/$asignaturaId/route'
|
||||||
|
import { Route as PlanesPlanIdAsignaturasListaNuevaRouteImport } from './routes/planes/$planId/asignaturas/_lista/nueva'
|
||||||
|
|
||||||
const Stepper2Route = Stepper2RouteImport.update({
|
|
||||||
id: '/stepper2',
|
|
||||||
path: '/stepper2',
|
|
||||||
getParentRoute: () => rootRouteImport,
|
|
||||||
} as any)
|
|
||||||
const StepperRoute = StepperRouteImport.update({
|
|
||||||
id: '/stepper',
|
|
||||||
path: '/stepper',
|
|
||||||
getParentRoute: () => rootRouteImport,
|
|
||||||
} as any)
|
|
||||||
const LoginRoute = LoginRouteImport.update({
|
const LoginRoute = LoginRouteImport.update({
|
||||||
id: '/login',
|
id: '/login',
|
||||||
path: '/login',
|
path: '/login',
|
||||||
@@ -57,75 +45,71 @@ const PlanesListaRouteRoute = PlanesListaRouteRouteImport.update({
|
|||||||
path: '/planes',
|
path: '/planes',
|
||||||
getParentRoute: () => rootRouteImport,
|
getParentRoute: () => rootRouteImport,
|
||||||
} as any)
|
} as any)
|
||||||
const PlanesPlanIdRouteRoute = PlanesPlanIdRouteRouteImport.update({
|
const PlanesPlanIdIndexRoute = PlanesPlanIdIndexRouteImport.update({
|
||||||
id: '/planes/$planId',
|
id: '/planes/$planId/',
|
||||||
path: '/planes/$planId',
|
path: '/planes/$planId/',
|
||||||
getParentRoute: () => rootRouteImport,
|
getParentRoute: () => rootRouteImport,
|
||||||
} as any)
|
} as any)
|
||||||
const AsignaturasListaRouteRoute = AsignaturasListaRouteRouteImport.update({
|
|
||||||
id: '/asignaturas/_lista',
|
|
||||||
path: '/asignaturas',
|
|
||||||
getParentRoute: () => rootRouteImport,
|
|
||||||
} as any)
|
|
||||||
const AsignaturasAsignaturaIdRouteRoute =
|
|
||||||
AsignaturasAsignaturaIdRouteRouteImport.update({
|
|
||||||
id: '/asignaturas/$asignaturaId',
|
|
||||||
path: '/asignaturas/$asignaturaId',
|
|
||||||
getParentRoute: () => rootRouteImport,
|
|
||||||
} as any)
|
|
||||||
const PlanesListaNuevoRoute = PlanesListaNuevoRouteImport.update({
|
const PlanesListaNuevoRoute = PlanesListaNuevoRouteImport.update({
|
||||||
id: '/nuevo',
|
id: '/nuevo',
|
||||||
path: '/nuevo',
|
path: '/nuevo',
|
||||||
getParentRoute: () => PlanesListaRouteRoute,
|
getParentRoute: () => PlanesListaRouteRoute,
|
||||||
} as any)
|
} as any)
|
||||||
const AsignaturasListaNuevaRoute = AsignaturasListaNuevaRouteImport.update({
|
const PlanesPlanIdAsignaturasListaRouteRoute =
|
||||||
id: '/nueva',
|
PlanesPlanIdAsignaturasListaRouteRouteImport.update({
|
||||||
path: '/nueva',
|
id: '/planes/$planId/asignaturas/_lista',
|
||||||
getParentRoute: () => AsignaturasListaRouteRoute,
|
path: '/planes/$planId/asignaturas',
|
||||||
} as any)
|
getParentRoute: () => rootRouteImport,
|
||||||
|
} as any)
|
||||||
|
const PlanesPlanIdAsignaturasAsignaturaIdRouteRoute =
|
||||||
|
PlanesPlanIdAsignaturasAsignaturaIdRouteRouteImport.update({
|
||||||
|
id: '/planes/$planId/asignaturas/$asignaturaId',
|
||||||
|
path: '/planes/$planId/asignaturas/$asignaturaId',
|
||||||
|
getParentRoute: () => rootRouteImport,
|
||||||
|
} as any)
|
||||||
|
const PlanesPlanIdAsignaturasListaNuevaRoute =
|
||||||
|
PlanesPlanIdAsignaturasListaNuevaRouteImport.update({
|
||||||
|
id: '/nueva',
|
||||||
|
path: '/nueva',
|
||||||
|
getParentRoute: () => PlanesPlanIdAsignaturasListaRouteRoute,
|
||||||
|
} as any)
|
||||||
|
|
||||||
export interface FileRoutesByFullPath {
|
export interface FileRoutesByFullPath {
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
'/dashboard': typeof DashboardRoute
|
'/dashboard': typeof DashboardRoute
|
||||||
'/login': typeof LoginRoute
|
'/login': typeof LoginRoute
|
||||||
'/stepper': typeof StepperRoute
|
|
||||||
'/stepper2': typeof Stepper2Route
|
|
||||||
'/asignaturas/$asignaturaId': typeof AsignaturasAsignaturaIdRouteRoute
|
|
||||||
'/asignaturas': typeof AsignaturasListaRouteRouteWithChildren
|
|
||||||
'/planes/$planId': typeof PlanesPlanIdRouteRoute
|
|
||||||
'/planes': typeof PlanesListaRouteRouteWithChildren
|
'/planes': typeof PlanesListaRouteRouteWithChildren
|
||||||
'/demo/tanstack-query': typeof DemoTanstackQueryRoute
|
'/demo/tanstack-query': typeof DemoTanstackQueryRoute
|
||||||
'/asignaturas/nueva': typeof AsignaturasListaNuevaRoute
|
|
||||||
'/planes/nuevo': typeof PlanesListaNuevoRoute
|
'/planes/nuevo': typeof PlanesListaNuevoRoute
|
||||||
|
'/planes/$planId': typeof PlanesPlanIdIndexRoute
|
||||||
|
'/planes/$planId/asignaturas/$asignaturaId': typeof PlanesPlanIdAsignaturasAsignaturaIdRouteRoute
|
||||||
|
'/planes/$planId/asignaturas': typeof PlanesPlanIdAsignaturasListaRouteRouteWithChildren
|
||||||
|
'/planes/$planId/asignaturas/nueva': typeof PlanesPlanIdAsignaturasListaNuevaRoute
|
||||||
}
|
}
|
||||||
export interface FileRoutesByTo {
|
export interface FileRoutesByTo {
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
'/dashboard': typeof DashboardRoute
|
'/dashboard': typeof DashboardRoute
|
||||||
'/login': typeof LoginRoute
|
'/login': typeof LoginRoute
|
||||||
'/stepper': typeof StepperRoute
|
|
||||||
'/stepper2': typeof Stepper2Route
|
|
||||||
'/asignaturas/$asignaturaId': typeof AsignaturasAsignaturaIdRouteRoute
|
|
||||||
'/asignaturas': typeof AsignaturasListaRouteRouteWithChildren
|
|
||||||
'/planes/$planId': typeof PlanesPlanIdRouteRoute
|
|
||||||
'/planes': typeof PlanesListaRouteRouteWithChildren
|
'/planes': typeof PlanesListaRouteRouteWithChildren
|
||||||
'/demo/tanstack-query': typeof DemoTanstackQueryRoute
|
'/demo/tanstack-query': typeof DemoTanstackQueryRoute
|
||||||
'/asignaturas/nueva': typeof AsignaturasListaNuevaRoute
|
|
||||||
'/planes/nuevo': typeof PlanesListaNuevoRoute
|
'/planes/nuevo': typeof PlanesListaNuevoRoute
|
||||||
|
'/planes/$planId': typeof PlanesPlanIdIndexRoute
|
||||||
|
'/planes/$planId/asignaturas/$asignaturaId': typeof PlanesPlanIdAsignaturasAsignaturaIdRouteRoute
|
||||||
|
'/planes/$planId/asignaturas': typeof PlanesPlanIdAsignaturasListaRouteRouteWithChildren
|
||||||
|
'/planes/$planId/asignaturas/nueva': typeof PlanesPlanIdAsignaturasListaNuevaRoute
|
||||||
}
|
}
|
||||||
export interface FileRoutesById {
|
export interface FileRoutesById {
|
||||||
__root__: typeof rootRouteImport
|
__root__: typeof rootRouteImport
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
'/dashboard': typeof DashboardRoute
|
'/dashboard': typeof DashboardRoute
|
||||||
'/login': typeof LoginRoute
|
'/login': typeof LoginRoute
|
||||||
'/stepper': typeof StepperRoute
|
|
||||||
'/stepper2': typeof Stepper2Route
|
|
||||||
'/asignaturas/$asignaturaId': typeof AsignaturasAsignaturaIdRouteRoute
|
|
||||||
'/asignaturas/_lista': typeof AsignaturasListaRouteRouteWithChildren
|
|
||||||
'/planes/$planId': typeof PlanesPlanIdRouteRoute
|
|
||||||
'/planes/_lista': typeof PlanesListaRouteRouteWithChildren
|
'/planes/_lista': typeof PlanesListaRouteRouteWithChildren
|
||||||
'/demo/tanstack-query': typeof DemoTanstackQueryRoute
|
'/demo/tanstack-query': typeof DemoTanstackQueryRoute
|
||||||
'/asignaturas/_lista/nueva': typeof AsignaturasListaNuevaRoute
|
|
||||||
'/planes/_lista/nuevo': typeof PlanesListaNuevoRoute
|
'/planes/_lista/nuevo': typeof PlanesListaNuevoRoute
|
||||||
|
'/planes/$planId/': typeof PlanesPlanIdIndexRoute
|
||||||
|
'/planes/$planId/asignaturas/$asignaturaId': typeof PlanesPlanIdAsignaturasAsignaturaIdRouteRoute
|
||||||
|
'/planes/$planId/asignaturas/_lista': typeof PlanesPlanIdAsignaturasListaRouteRouteWithChildren
|
||||||
|
'/planes/$planId/asignaturas/_lista/nueva': typeof PlanesPlanIdAsignaturasListaNuevaRoute
|
||||||
}
|
}
|
||||||
export interface FileRouteTypes {
|
export interface FileRouteTypes {
|
||||||
fileRoutesByFullPath: FileRoutesByFullPath
|
fileRoutesByFullPath: FileRoutesByFullPath
|
||||||
@@ -133,74 +117,52 @@ export interface FileRouteTypes {
|
|||||||
| '/'
|
| '/'
|
||||||
| '/dashboard'
|
| '/dashboard'
|
||||||
| '/login'
|
| '/login'
|
||||||
| '/stepper'
|
|
||||||
| '/stepper2'
|
|
||||||
| '/asignaturas/$asignaturaId'
|
|
||||||
| '/asignaturas'
|
|
||||||
| '/planes/$planId'
|
|
||||||
| '/planes'
|
| '/planes'
|
||||||
| '/demo/tanstack-query'
|
| '/demo/tanstack-query'
|
||||||
| '/asignaturas/nueva'
|
|
||||||
| '/planes/nuevo'
|
| '/planes/nuevo'
|
||||||
|
| '/planes/$planId'
|
||||||
|
| '/planes/$planId/asignaturas/$asignaturaId'
|
||||||
|
| '/planes/$planId/asignaturas'
|
||||||
|
| '/planes/$planId/asignaturas/nueva'
|
||||||
fileRoutesByTo: FileRoutesByTo
|
fileRoutesByTo: FileRoutesByTo
|
||||||
to:
|
to:
|
||||||
| '/'
|
| '/'
|
||||||
| '/dashboard'
|
| '/dashboard'
|
||||||
| '/login'
|
| '/login'
|
||||||
| '/stepper'
|
|
||||||
| '/stepper2'
|
|
||||||
| '/asignaturas/$asignaturaId'
|
|
||||||
| '/asignaturas'
|
|
||||||
| '/planes/$planId'
|
|
||||||
| '/planes'
|
| '/planes'
|
||||||
| '/demo/tanstack-query'
|
| '/demo/tanstack-query'
|
||||||
| '/asignaturas/nueva'
|
|
||||||
| '/planes/nuevo'
|
| '/planes/nuevo'
|
||||||
|
| '/planes/$planId'
|
||||||
|
| '/planes/$planId/asignaturas/$asignaturaId'
|
||||||
|
| '/planes/$planId/asignaturas'
|
||||||
|
| '/planes/$planId/asignaturas/nueva'
|
||||||
id:
|
id:
|
||||||
| '__root__'
|
| '__root__'
|
||||||
| '/'
|
| '/'
|
||||||
| '/dashboard'
|
| '/dashboard'
|
||||||
| '/login'
|
| '/login'
|
||||||
| '/stepper'
|
|
||||||
| '/stepper2'
|
|
||||||
| '/asignaturas/$asignaturaId'
|
|
||||||
| '/asignaturas/_lista'
|
|
||||||
| '/planes/$planId'
|
|
||||||
| '/planes/_lista'
|
| '/planes/_lista'
|
||||||
| '/demo/tanstack-query'
|
| '/demo/tanstack-query'
|
||||||
| '/asignaturas/_lista/nueva'
|
|
||||||
| '/planes/_lista/nuevo'
|
| '/planes/_lista/nuevo'
|
||||||
|
| '/planes/$planId/'
|
||||||
|
| '/planes/$planId/asignaturas/$asignaturaId'
|
||||||
|
| '/planes/$planId/asignaturas/_lista'
|
||||||
|
| '/planes/$planId/asignaturas/_lista/nueva'
|
||||||
fileRoutesById: FileRoutesById
|
fileRoutesById: FileRoutesById
|
||||||
}
|
}
|
||||||
export interface RootRouteChildren {
|
export interface RootRouteChildren {
|
||||||
IndexRoute: typeof IndexRoute
|
IndexRoute: typeof IndexRoute
|
||||||
DashboardRoute: typeof DashboardRoute
|
DashboardRoute: typeof DashboardRoute
|
||||||
LoginRoute: typeof LoginRoute
|
LoginRoute: typeof LoginRoute
|
||||||
StepperRoute: typeof StepperRoute
|
|
||||||
Stepper2Route: typeof Stepper2Route
|
|
||||||
AsignaturasAsignaturaIdRouteRoute: typeof AsignaturasAsignaturaIdRouteRoute
|
|
||||||
AsignaturasListaRouteRoute: typeof AsignaturasListaRouteRouteWithChildren
|
|
||||||
PlanesPlanIdRouteRoute: typeof PlanesPlanIdRouteRoute
|
|
||||||
PlanesListaRouteRoute: typeof PlanesListaRouteRouteWithChildren
|
PlanesListaRouteRoute: typeof PlanesListaRouteRouteWithChildren
|
||||||
DemoTanstackQueryRoute: typeof DemoTanstackQueryRoute
|
DemoTanstackQueryRoute: typeof DemoTanstackQueryRoute
|
||||||
|
PlanesPlanIdIndexRoute: typeof PlanesPlanIdIndexRoute
|
||||||
|
PlanesPlanIdAsignaturasAsignaturaIdRouteRoute: typeof PlanesPlanIdAsignaturasAsignaturaIdRouteRoute
|
||||||
|
PlanesPlanIdAsignaturasListaRouteRoute: typeof PlanesPlanIdAsignaturasListaRouteRouteWithChildren
|
||||||
}
|
}
|
||||||
|
|
||||||
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': {
|
|
||||||
id: '/stepper'
|
|
||||||
path: '/stepper'
|
|
||||||
fullPath: '/stepper'
|
|
||||||
preLoaderRoute: typeof StepperRouteImport
|
|
||||||
parentRoute: typeof rootRouteImport
|
|
||||||
}
|
|
||||||
'/login': {
|
'/login': {
|
||||||
id: '/login'
|
id: '/login'
|
||||||
path: '/login'
|
path: '/login'
|
||||||
@@ -236,25 +198,11 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof PlanesListaRouteRouteImport
|
preLoaderRoute: typeof PlanesListaRouteRouteImport
|
||||||
parentRoute: typeof rootRouteImport
|
parentRoute: typeof rootRouteImport
|
||||||
}
|
}
|
||||||
'/planes/$planId': {
|
'/planes/$planId/': {
|
||||||
id: '/planes/$planId'
|
id: '/planes/$planId/'
|
||||||
path: '/planes/$planId'
|
path: '/planes/$planId'
|
||||||
fullPath: '/planes/$planId'
|
fullPath: '/planes/$planId'
|
||||||
preLoaderRoute: typeof PlanesPlanIdRouteRouteImport
|
preLoaderRoute: typeof PlanesPlanIdIndexRouteImport
|
||||||
parentRoute: typeof rootRouteImport
|
|
||||||
}
|
|
||||||
'/asignaturas/_lista': {
|
|
||||||
id: '/asignaturas/_lista'
|
|
||||||
path: '/asignaturas'
|
|
||||||
fullPath: '/asignaturas'
|
|
||||||
preLoaderRoute: typeof AsignaturasListaRouteRouteImport
|
|
||||||
parentRoute: typeof rootRouteImport
|
|
||||||
}
|
|
||||||
'/asignaturas/$asignaturaId': {
|
|
||||||
id: '/asignaturas/$asignaturaId'
|
|
||||||
path: '/asignaturas/$asignaturaId'
|
|
||||||
fullPath: '/asignaturas/$asignaturaId'
|
|
||||||
preLoaderRoute: typeof AsignaturasAsignaturaIdRouteRouteImport
|
|
||||||
parentRoute: typeof rootRouteImport
|
parentRoute: typeof rootRouteImport
|
||||||
}
|
}
|
||||||
'/planes/_lista/nuevo': {
|
'/planes/_lista/nuevo': {
|
||||||
@@ -264,29 +212,30 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof PlanesListaNuevoRouteImport
|
preLoaderRoute: typeof PlanesListaNuevoRouteImport
|
||||||
parentRoute: typeof PlanesListaRouteRoute
|
parentRoute: typeof PlanesListaRouteRoute
|
||||||
}
|
}
|
||||||
'/asignaturas/_lista/nueva': {
|
'/planes/$planId/asignaturas/_lista': {
|
||||||
id: '/asignaturas/_lista/nueva'
|
id: '/planes/$planId/asignaturas/_lista'
|
||||||
|
path: '/planes/$planId/asignaturas'
|
||||||
|
fullPath: '/planes/$planId/asignaturas'
|
||||||
|
preLoaderRoute: typeof PlanesPlanIdAsignaturasListaRouteRouteImport
|
||||||
|
parentRoute: typeof rootRouteImport
|
||||||
|
}
|
||||||
|
'/planes/$planId/asignaturas/$asignaturaId': {
|
||||||
|
id: '/planes/$planId/asignaturas/$asignaturaId'
|
||||||
|
path: '/planes/$planId/asignaturas/$asignaturaId'
|
||||||
|
fullPath: '/planes/$planId/asignaturas/$asignaturaId'
|
||||||
|
preLoaderRoute: typeof PlanesPlanIdAsignaturasAsignaturaIdRouteRouteImport
|
||||||
|
parentRoute: typeof rootRouteImport
|
||||||
|
}
|
||||||
|
'/planes/$planId/asignaturas/_lista/nueva': {
|
||||||
|
id: '/planes/$planId/asignaturas/_lista/nueva'
|
||||||
path: '/nueva'
|
path: '/nueva'
|
||||||
fullPath: '/asignaturas/nueva'
|
fullPath: '/planes/$planId/asignaturas/nueva'
|
||||||
preLoaderRoute: typeof AsignaturasListaNuevaRouteImport
|
preLoaderRoute: typeof PlanesPlanIdAsignaturasListaNuevaRouteImport
|
||||||
parentRoute: typeof AsignaturasListaRouteRoute
|
parentRoute: typeof PlanesPlanIdAsignaturasListaRouteRoute
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AsignaturasListaRouteRouteChildren {
|
|
||||||
AsignaturasListaNuevaRoute: typeof AsignaturasListaNuevaRoute
|
|
||||||
}
|
|
||||||
|
|
||||||
const AsignaturasListaRouteRouteChildren: AsignaturasListaRouteRouteChildren = {
|
|
||||||
AsignaturasListaNuevaRoute: AsignaturasListaNuevaRoute,
|
|
||||||
}
|
|
||||||
|
|
||||||
const AsignaturasListaRouteRouteWithChildren =
|
|
||||||
AsignaturasListaRouteRoute._addFileChildren(
|
|
||||||
AsignaturasListaRouteRouteChildren,
|
|
||||||
)
|
|
||||||
|
|
||||||
interface PlanesListaRouteRouteChildren {
|
interface PlanesListaRouteRouteChildren {
|
||||||
PlanesListaNuevoRoute: typeof PlanesListaNuevoRoute
|
PlanesListaNuevoRoute: typeof PlanesListaNuevoRoute
|
||||||
}
|
}
|
||||||
@@ -298,17 +247,32 @@ const PlanesListaRouteRouteChildren: PlanesListaRouteRouteChildren = {
|
|||||||
const PlanesListaRouteRouteWithChildren =
|
const PlanesListaRouteRouteWithChildren =
|
||||||
PlanesListaRouteRoute._addFileChildren(PlanesListaRouteRouteChildren)
|
PlanesListaRouteRoute._addFileChildren(PlanesListaRouteRouteChildren)
|
||||||
|
|
||||||
|
interface PlanesPlanIdAsignaturasListaRouteRouteChildren {
|
||||||
|
PlanesPlanIdAsignaturasListaNuevaRoute: typeof PlanesPlanIdAsignaturasListaNuevaRoute
|
||||||
|
}
|
||||||
|
|
||||||
|
const PlanesPlanIdAsignaturasListaRouteRouteChildren: PlanesPlanIdAsignaturasListaRouteRouteChildren =
|
||||||
|
{
|
||||||
|
PlanesPlanIdAsignaturasListaNuevaRoute:
|
||||||
|
PlanesPlanIdAsignaturasListaNuevaRoute,
|
||||||
|
}
|
||||||
|
|
||||||
|
const PlanesPlanIdAsignaturasListaRouteRouteWithChildren =
|
||||||
|
PlanesPlanIdAsignaturasListaRouteRoute._addFileChildren(
|
||||||
|
PlanesPlanIdAsignaturasListaRouteRouteChildren,
|
||||||
|
)
|
||||||
|
|
||||||
const rootRouteChildren: RootRouteChildren = {
|
const rootRouteChildren: RootRouteChildren = {
|
||||||
IndexRoute: IndexRoute,
|
IndexRoute: IndexRoute,
|
||||||
DashboardRoute: DashboardRoute,
|
DashboardRoute: DashboardRoute,
|
||||||
LoginRoute: LoginRoute,
|
LoginRoute: LoginRoute,
|
||||||
StepperRoute: StepperRoute,
|
|
||||||
Stepper2Route: Stepper2Route,
|
|
||||||
AsignaturasAsignaturaIdRouteRoute: AsignaturasAsignaturaIdRouteRoute,
|
|
||||||
AsignaturasListaRouteRoute: AsignaturasListaRouteRouteWithChildren,
|
|
||||||
PlanesPlanIdRouteRoute: PlanesPlanIdRouteRoute,
|
|
||||||
PlanesListaRouteRoute: PlanesListaRouteRouteWithChildren,
|
PlanesListaRouteRoute: PlanesListaRouteRouteWithChildren,
|
||||||
DemoTanstackQueryRoute: DemoTanstackQueryRoute,
|
DemoTanstackQueryRoute: DemoTanstackQueryRoute,
|
||||||
|
PlanesPlanIdIndexRoute: PlanesPlanIdIndexRoute,
|
||||||
|
PlanesPlanIdAsignaturasAsignaturaIdRouteRoute:
|
||||||
|
PlanesPlanIdAsignaturasAsignaturaIdRouteRoute,
|
||||||
|
PlanesPlanIdAsignaturasListaRouteRoute:
|
||||||
|
PlanesPlanIdAsignaturasListaRouteRouteWithChildren,
|
||||||
}
|
}
|
||||||
export const routeTree = rootRouteImport
|
export const routeTree = rootRouteImport
|
||||||
._addFileChildren(rootRouteChildren)
|
._addFileChildren(rootRouteChildren)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { createFileRoute } from '@tanstack/react-router'
|
import { createFileRoute } from '@tanstack/react-router'
|
||||||
|
|
||||||
export const Route = createFileRoute('/asignaturas/$asignaturaId')({
|
export const Route = createFileRoute('/planes/$planId/asignaturas/$asignaturaId')({
|
||||||
component: RouteComponent,
|
component: RouteComponent,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -35,7 +35,9 @@ import {
|
|||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from '@/components/ui/tooltip'
|
} from '@/components/ui/tooltip'
|
||||||
|
|
||||||
export const Route = createFileRoute('/asignaturas/_lista/nueva')({
|
export const Route = createFileRoute(
|
||||||
|
'/planes/$planId/asignaturas/_lista/nueva',
|
||||||
|
)({
|
||||||
component: NuevaMateriaModal,
|
component: NuevaMateriaModal,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -54,7 +56,7 @@ type MateriaPreview = {
|
|||||||
|
|
||||||
type NewSubjectWizardState = {
|
type NewSubjectWizardState = {
|
||||||
step: 1 | 2 | 3 | 4
|
step: 1 | 2 | 3 | 4
|
||||||
// planId: string
|
planId: string
|
||||||
modoCreacion: ModoCreacion | null
|
modoCreacion: ModoCreacion | null
|
||||||
subModoClonado?: SubModoClonado
|
subModoClonado?: SubModoClonado
|
||||||
datosBasicos: {
|
datosBasicos: {
|
||||||
@@ -175,12 +177,12 @@ const Wizard = defineStepper(
|
|||||||
|
|
||||||
function NuevaMateriaModal() {
|
function NuevaMateriaModal() {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
// const { planId } = Route.useParams()
|
const { planId } = Route.useParams()
|
||||||
const role = auth_get_current_user_role()
|
const role = auth_get_current_user_role()
|
||||||
|
|
||||||
const [wizard, setWizard] = useState<NewSubjectWizardState>({
|
const [wizard, setWizard] = useState<NewSubjectWizardState>({
|
||||||
step: 1,
|
step: 1,
|
||||||
// planId: planId,
|
planId: planId,
|
||||||
modoCreacion: null,
|
modoCreacion: null,
|
||||||
datosBasicos: {
|
datosBasicos: {
|
||||||
nombre: '',
|
nombre: '',
|
||||||
@@ -207,7 +209,7 @@ function NuevaMateriaModal() {
|
|||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
// Redirige a la pestaña de materias del plan
|
// Redirige a la pestaña de materias del plan
|
||||||
navigate({ to: `/planes`, resetScroll: false })
|
navigate({ to: `/planes/${planId}/asignaturas`, resetScroll: false })
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Validaciones ---
|
// --- Validaciones ---
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { createFileRoute, Outlet } from '@tanstack/react-router'
|
import { createFileRoute, Outlet } from '@tanstack/react-router'
|
||||||
|
|
||||||
export const Route = createFileRoute('/asignaturas/_lista')({
|
export const Route = createFileRoute('/planes/$planId/asignaturas/_lista')({
|
||||||
component: RouteComponent,
|
component: RouteComponent,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -8,6 +8,7 @@ function RouteComponent() {
|
|||||||
return (
|
return (
|
||||||
<main className="bg-background min-h-screen w-full">
|
<main className="bg-background min-h-screen w-full">
|
||||||
<div className="mx-auto flex w-full max-w-7xl flex-col gap-4 px-4 py-6 md:px-6 lg:px-8">
|
<div className="mx-auto flex w-full max-w-7xl flex-col gap-4 px-4 py-6 md:px-6 lg:px-8">
|
||||||
|
<h1 className="text-foreground text-2xl font-semibold">Asignaturas</h1>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { createFileRoute } from '@tanstack/react-router'
|
import { createFileRoute } from '@tanstack/react-router'
|
||||||
|
|
||||||
export const Route = createFileRoute('/planes/$planId')({
|
export const Route = createFileRoute('/planes/$planId/')({
|
||||||
component: RouteComponent,
|
component: RouteComponent,
|
||||||
})
|
})
|
||||||
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,65 +0,0 @@
|
|||||||
import { createFileRoute } from '@tanstack/react-router'
|
|
||||||
|
|
||||||
import { defineStepper } from '@/components/stepper'
|
|
||||||
import { Button } from '@/components/ui/button'
|
|
||||||
|
|
||||||
export const Route = createFileRoute('/stepper')({
|
|
||||||
component: MyFirstStepper,
|
|
||||||
})
|
|
||||||
|
|
||||||
const stepperInstance = defineStepper(
|
|
||||||
{ id: 'step-1', title: 'Step 1' },
|
|
||||||
{ id: 'step-2', title: 'Step 2' },
|
|
||||||
{ id: 'step-3', title: 'Step 3' },
|
|
||||||
)
|
|
||||||
|
|
||||||
export function MyFirstStepper() {
|
|
||||||
return (
|
|
||||||
<stepperInstance.Stepper.Provider className="space-y-4">
|
|
||||||
{({ methods }) => (
|
|
||||||
<>
|
|
||||||
<stepperInstance.Stepper.Navigation>
|
|
||||||
{methods.all.map((step) => (
|
|
||||||
<stepperInstance.Stepper.Step
|
|
||||||
of={step.id}
|
|
||||||
onClick={() => methods.goTo(step.id)}
|
|
||||||
>
|
|
||||||
<stepperInstance.Stepper.Title>
|
|
||||||
{step.title}
|
|
||||||
</stepperInstance.Stepper.Title>
|
|
||||||
</stepperInstance.Stepper.Step>
|
|
||||||
))}
|
|
||||||
</stepperInstance.Stepper.Navigation>
|
|
||||||
{methods.switch({
|
|
||||||
'step-1': (step) => <Content id={step.id} />,
|
|
||||||
'step-2': (step) => <Content id={step.id} />,
|
|
||||||
'step-3': (step) => <Content id={step.id} />,
|
|
||||||
})}
|
|
||||||
<stepperInstance.Stepper.Controls>
|
|
||||||
{!methods.isLast && (
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="secondary"
|
|
||||||
onClick={methods.prev}
|
|
||||||
disabled={methods.isFirst}
|
|
||||||
>
|
|
||||||
Previous
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
<Button onClick={methods.isLast ? methods.reset : methods.next}>
|
|
||||||
{methods.isLast ? 'Reset' : 'Next'}
|
|
||||||
</Button>
|
|
||||||
</stepperInstance.Stepper.Controls>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</stepperInstance.Stepper.Provider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const Content = ({ id }: { id: string }) => {
|
|
||||||
return (
|
|
||||||
<stepperInstance.Stepper.Panel className="h-50 content-center rounded border bg-slate-50 p-8">
|
|
||||||
<p className="text-xl font-normal">Content for {id}</p>
|
|
||||||
</stepperInstance.Stepper.Panel>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
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