Merge remote-tracking branch 'origin/issue/45-integrar-el-wizard-de-creacin-de-materia' into issue/42-que-tenga-persistencia-el-plan-de-estudios

This commit is contained in:
2026-02-04 07:29:35 -06:00
24 changed files with 1698 additions and 1576 deletions

View File

@@ -23,43 +23,41 @@ export function WizardControls({
const isLast = idx >= Wizard.steps.length - 1
return (
<div className="flex-none border-t bg-white p-6">
<div 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 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 className="flex gap-4">
<div className="flex gap-4">
<Button
variant="secondary"
onClick={() => methods.prev()}
disabled={idx === 0 || wizard.isLoading}
>
Anterior
</Button>
{!isLast ? (
<Button
variant="secondary"
onClick={() => methods.prev()}
disabled={idx === 0 || wizard.isLoading}
onClick={() => methods.next()}
disabled={
wizard.isLoading ||
(idx === 0 && !canContinueDesdeMetodo) ||
(idx === 1 && !canContinueDesdeBasicos) ||
(idx === 2 && !canContinueDesdeConfig)
}
>
Anterior
Siguiente
</Button>
{!isLast ? (
<Button
onClick={() => methods.next()}
disabled={
wizard.isLoading ||
(idx === 0 && !canContinueDesdeMetodo) ||
(idx === 1 && !canContinueDesdeBasicos) ||
(idx === 2 && !canContinueDesdeConfig)
}
>
Siguiente
</Button>
) : (
<Button onClick={onCreate} disabled={wizard.isLoading}>
{wizard.isLoading ? 'Creando...' : 'Crear Asignatura'}
</Button>
)}
</div>
) : (
<Button onClick={onCreate} disabled={wizard.isLoading}>
{wizard.isLoading ? 'Creando...' : 'Crear Asignatura'}
</Button>
)}
</div>
</div>
)

View File

@@ -34,7 +34,7 @@ export function PasoBasicosForm({
const estructurasPlanList = catalogos?.estructurasPlan ?? []
const filteredCarreras = rawCarreras.filter((c: any) => {
const facId = wizard.datosBasicos.facultadId
const facId = wizard.datosBasicos.facultad.id
if (!facId) return true
// soportar ambos shapes: `facultad_id` (BD) o `facultadId` (local)
return c.facultad_id ? c.facultad_id === facId : c.facultadId === facId
@@ -68,15 +68,20 @@ export function PasoBasicosForm({
<div className="grid gap-1">
<Label htmlFor="facultad">Facultad</Label>
<Select
value={wizard.datosBasicos.facultadId}
value={wizard.datosBasicos.facultad.id}
onValueChange={(value) =>
onChange(
(w): NewPlanWizardState => ({
...w,
datosBasicos: {
...w.datosBasicos,
facultadId: value,
carreraId: '',
facultad: {
id: value,
nombre:
facultadesList.find((f) => f.id === value)?.nombre ||
'',
},
carrera: { id: '', nombre: '' },
},
}),
)
@@ -86,7 +91,7 @@ export function PasoBasicosForm({
id="facultad"
className={cn(
'w-full min-w-0 [&>span]:block! [&>span]:truncate!',
!wizard.datosBasicos.facultadId
!wizard.datosBasicos.facultad.id
? 'text-muted-foreground font-normal italic opacity-70' // Es Placeholder
: 'font-medium not-italic', // Tiene Valor (Medium)
)}
@@ -106,22 +111,30 @@ export function PasoBasicosForm({
<div className="grid gap-1">
<Label htmlFor="carrera">Carrera</Label>
<Select
value={wizard.datosBasicos.carreraId}
value={wizard.datosBasicos.carrera.id}
onValueChange={(value) =>
onChange(
(w): NewPlanWizardState => ({
...w,
datosBasicos: { ...w.datosBasicos, carreraId: value },
datosBasicos: {
...w.datosBasicos,
carrera: {
id: value,
nombre:
filteredCarreras.find((c) => c.id === value)?.nombre ||
'',
},
},
}),
)
}
disabled={!wizard.datosBasicos.facultadId}
disabled={!wizard.datosBasicos.facultad.id}
>
<SelectTrigger
id="carrera"
className={cn(
'w-full min-w-0 [&>span]:block! [&>span]:truncate!',
!wizard.datosBasicos.carreraId
!wizard.datosBasicos.carrera.id
? 'text-muted-foreground font-normal italic opacity-70' // Es Placeholder
: 'font-medium not-italic', // Tiene Valor (Medium)
)}

View File

@@ -79,7 +79,7 @@ export function PasoDetallesPanel({
...w,
iaConfig: {
...(w.iaConfig || ({} as any)),
InstruccionesAdicionalesIA: e.target.value,
instruccionesAdicionalesIA: e.target.value,
},
}))
}

View File

@@ -17,6 +17,7 @@ import {
TabsContents,
} from '@/components/ui/motion-tabs'
import { ARCHIVOS, REPOSITORIOS } from '@/features/planes/nuevo/catalogs'
import { cn } from '@/lib/utils'
const ReferenciasParaIA = ({
selectedArchivoIds = [],
@@ -87,7 +88,10 @@ const ReferenciasParaIA = ({
onCheckedChange={(checked) =>
onToggleArchivo?.(archivo.id, !!checked)
}
className="peer border-primary ring-offset-background data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground focus-visible:ring-ring h-5 w-5 shrink-0 rounded-sm border focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50"
className={cn(
'peer border-primary ring-offset-background data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground focus-visible:ring-ring h-5 w-5 shrink-0 rounded-sm border focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50',
selectedArchivoIds.includes(archivo.id) ? '' : 'invisible',
)}
/>
<FileText className="text-muted-foreground h-4 w-4" />
@@ -134,7 +138,12 @@ const ReferenciasParaIA = ({
onCheckedChange={(checked) =>
onToggleRepositorio?.(repositorio.id, !!checked)
}
className="peer border-primary ring-offset-background data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground focus-visible:ring-ring h-5 w-5 shrink-0 rounded-sm border focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50"
className={cn(
'peer border-primary ring-offset-background data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground focus-visible:ring-ring h-5 w-5 shrink-0 rounded-sm border focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50',
selectedRepositorioIds.includes(repositorio.id)
? ''
: 'invisible',
)}
/>
<FolderOpen className="text-muted-foreground h-4 w-4" />

View File

@@ -45,8 +45,8 @@ export function PasoResumenCard({ wizard }: { wizard: NewPlanWizardState }) {
Facultad/Carrera:{' '}
</span>
<span className="font-medium">
{wizard.datosBasicos.facultadId || '—'} /{' '}
{wizard.datosBasicos.carreraId || '—'}
{wizard.datosBasicos.facultad.nombre || '—'} /{' '}
{wizard.datosBasicos.carrera.nombre || '—'}
</span>
</div>
<div>

View File

@@ -57,8 +57,8 @@ export function WizardControls({
const aiInput = {
datosBasicos: {
nombrePlan: wizard.datosBasicos.nombrePlan,
carreraId: wizard.datosBasicos.carreraId,
facultadId: wizard.datosBasicos.facultadId || undefined,
carreraId: wizard.datosBasicos.carrera.id || undefined,
facultadId: wizard.datosBasicos.facultad.id || undefined,
nivel: wizard.datosBasicos.nivel as string,
tipoCiclo: tipoCicloSafe,
numCiclos: numCiclosSafe,
@@ -81,7 +81,7 @@ export function WizardControls({
console.log(`${new Date().toISOString()} - Plan IA generado`, data)
navigate({
to: `/planes/${data.plan.id}/datos`,
to: `/planes/${data.plan.id}`,
state: { showConfetti: true },
})
return
@@ -90,7 +90,7 @@ export function WizardControls({
if (wizard.tipoOrigen === 'MANUAL') {
// Crear plan vacío manualmente usando el hook
const plan = await createPlanManual.mutateAsync({
carreraId: wizard.datosBasicos.carreraId,
carreraId: wizard.datosBasicos.carrera.id,
estructuraId: wizard.datosBasicos.estructuraPlanId as string,
nombre: wizard.datosBasicos.nombrePlan,
nivel: wizard.datosBasicos.nivel as NivelPlanEstudio,
@@ -101,7 +101,7 @@ export function WizardControls({
// Navegar al nuevo plan
navigate({
to: `/planes/${plan.id}/datos`,
to: `/planes/${plan.id}`,
state: { showConfetti: true },
})
return

View 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-50 text-xs">
<p>{desc}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)
}

View File

@@ -0,0 +1,52 @@
import * as Icons from 'lucide-react'
import { CardHeader, CardTitle } from '@/components/ui/card'
import { Dialog, DialogContent } from '@/components/ui/dialog'
export function WizardLayout({
title,
onClose,
headerSlot,
footerSlot,
children,
}: {
title: string
onClose: () => void
headerSlot?: React.ReactNode
footerSlot?: React.ReactNode
children: React.ReactNode
}) {
return (
<Dialog open={true} onOpenChange={(open) => !open && onClose()}>
<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()
}}
>
<div className="z-10 flex-none border-b bg-white">
<CardHeader className="flex flex-row items-center justify-between gap-4 p-6 pb-4">
<CardTitle>{title}</CardTitle>
<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>
</CardHeader>
{headerSlot ? <div className="px-6 pb-6">{headerSlot}</div> : null}
</div>
<div className="flex-1 overflow-y-auto bg-gray-50/30 p-6">
{children}
</div>
{footerSlot ? (
<div className="flex-none border-t bg-white p-6">{footerSlot}</div>
) : null}
</DialogContent>
</Dialog>
)
}

View File

@@ -0,0 +1,59 @@
import { CircularProgress } from '@/components/CircularProgress'
import { StepWithTooltip } from '@/components/wizard/StepWithTooltip'
export function WizardResponsiveHeader({
wizard,
methods,
}: {
wizard: any
methods: any
}) {
const idx = wizard.utils.getIndex(methods.current.id)
const totalSteps = wizard.steps.length
const currentIndex = idx + 1
const hasNextStep = idx < totalSteps - 1
const nextStep = wizard.steps[currentIndex]
return (
<>
<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={methods.current.title}
desc={methods.current.description}
/>
</h2>
{hasNextStep && nextStep ? (
<p className="text-sm text-slate-400">
Siguiente: {nextStep.title}
</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>
</>
)
}

View File

@@ -1,31 +1,33 @@
export const qk = {
auth: ["auth"] as const,
session: () => ["auth", "session"] as const,
meProfile: () => ["auth", "meProfile"] as const,
auth: ['auth'] as const,
session: () => ['auth', 'session'] as const,
meProfile: () => ['auth', 'meProfile'] as const,
facultades: () => ["meta", "facultades"] as const,
facultades: () => ['meta', 'facultades'] as const,
carreras: (facultadId?: string | null) =>
["meta", "carreras", { facultadId: facultadId ?? null }] as const,
['meta', 'carreras', { facultadId: facultadId ?? null }] as const,
estructurasPlan: (nivel?: string | null) =>
["meta", "estructurasPlan", { nivel: nivel ?? null }] as const,
estructurasAsignatura: () => ["meta", "estructurasAsignatura"] as const,
estadosPlan: () => ["meta", "estadosPlan"] as const,
['meta', 'estructurasPlan', { nivel: nivel ?? null }] as const,
estructurasAsignatura: () => ['meta', 'estructurasAsignatura'] as const,
estadosPlan: () => ['meta', 'estadosPlan'] as const,
planesList: (filters: unknown) => ["planes", "list", filters] as const,
plan: (planId: string) => ["planes", "detail", planId] as const,
planLineas: (planId: string) => ["planes", planId, "lineas"] as const,
planAsignaturas: (planId: string) => ["planes", planId, "asignaturas"] as const,
planHistorial: (planId: string) => ["planes", planId, "historial"] as const,
planDocumento: (planId: string) => ["planes", planId, "documento"] as const,
planesList: (filters: unknown) => ['planes', 'list', filters] as const,
plan: (planId: string) => ['planes', 'detail', planId] as const,
planLineas: (planId: string) => ['planes', planId, 'lineas'] as const,
planAsignaturas: (planId: string) =>
['planes', planId, 'asignaturas'] as const,
planHistorial: (planId: string) => ['planes', planId, 'historial'] as const,
planDocumento: (planId: string) => ['planes', planId, 'documento'] as const,
asignatura: (asignaturaId: string) => ["asignaturas", "detail", asignaturaId] as const,
asignatura: (asignaturaId: string) =>
['asignaturas', 'detail', asignaturaId] as const,
asignaturaBibliografia: (asignaturaId: string) =>
["asignaturas", asignaturaId, "bibliografia"] as const,
['asignaturas', asignaturaId, 'bibliografia'] as const,
asignaturaHistorial: (asignaturaId: string) =>
["asignaturas", asignaturaId, "historial"] as const,
['asignaturas', asignaturaId, 'historial'] as const,
asignaturaDocumento: (asignaturaId: string) =>
["asignaturas", asignaturaId, "documento"] as const,
['asignaturas', asignaturaId, 'documento'] as const,
tareas: () => ["tareas", "mias"] as const,
notificaciones: () => ["notificaciones", "mias"] as const,
};
tareas: () => ['tareas', 'mias'] as const,
notificaciones: () => ['notificaciones', 'mias'] as const,
}

View File

@@ -1,4 +1,5 @@
import { useNavigate } from '@tanstack/react-router'
import * as Icons from 'lucide-react'
import { useNuevaAsignaturaWizard } from './hooks/useNuevaAsignaturaWizard'
@@ -6,11 +7,20 @@ import { PasoBasicosForm } from '@/components/asignaturas/wizard/PasoBasicosForm
import { PasoConfiguracionPanel } from '@/components/asignaturas/wizard/PasoConfiguracionPanel'
import { PasoMetodoCardGroup } from '@/components/asignaturas/wizard/PasoMetodoCardGroup'
import { PasoResumenCard } from '@/components/asignaturas/wizard/PasoResumenCard'
import { VistaSinPermisos } from '@/components/asignaturas/wizard/VistaSinPermisos'
import { WizardControls } from '@/components/asignaturas/wizard/WizardControls'
import { WizardHeader } from '@/components/asignaturas/wizard/WizardHeader'
import { defineStepper } from '@/components/stepper'
import { Dialog, DialogContent } from '@/components/ui/dialog'
import { Button } from '@/components/ui/button'
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/components/ui/card'
import { WizardLayout } from '@/components/wizard/WizardLayout'
import { WizardResponsiveHeader } from '@/components/wizard/WizardResponsiveHeader'
const auth_get_current_user_role = (): string => 'JEFE_CARRERA'
const Wizard = defineStepper(
{
@@ -24,8 +34,8 @@ const Wizard = defineStepper(
description: 'Nombre y estructura',
},
{
id: 'configuracion',
title: 'Configuración',
id: 'detalles',
title: 'Detalles',
description: 'Detalles según modo',
},
{
@@ -35,8 +45,6 @@ const Wizard = defineStepper(
},
)
const auth_get_current_user_role = () => 'JEFE_CARRERA' as const
export function NuevaAsignaturaModalContainer({ planId }: { planId: string }) {
const navigate = useNavigate()
const role = auth_get_current_user_role()
@@ -55,59 +63,46 @@ export function NuevaAsignaturaModalContainer({ planId }: { planId: string }) {
navigate({ to: `/planes/${planId}/asignaturas`, resetScroll: false })
}
if (role !== 'JEFE_CARRERA') {
return (
<WizardLayout title="Nueva Asignatura" onClose={handleClose}>
<Card className="border-destructive/40">
<CardHeader>
<CardTitle className="text-destructive flex items-center gap-2">
<Icons.ShieldAlert className="h-5 w-5" />
Sin permisos
</CardTitle>
<CardDescription>
Solo el Jefe de Carrera puede crear asignaturas.
</CardDescription>
</CardHeader>
<CardContent className="flex justify-end">
<Button variant="secondary" onClick={handleClose}>
Volver
</Button>
</CardContent>
</Card>
</WizardLayout>
)
}
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' ? (
<VistaSinPermisos onClose={handleClose} />
) : (
<Wizard.Stepper.Provider
initialStep={Wizard.utils.getFirst().id}
className="flex h-full flex-col"
>
{({ methods }) => (
<>
<WizardHeader
title="Nueva Asignatura"
Wizard={Wizard}
methods={{ ...methods, onClose: handleClose }}
/>
<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>
<PasoMetodoCardGroup
wizard={wizard}
onChange={setWizard}
/>
</Wizard.Stepper.Panel>
)}
{Wizard.utils.getIndex(methods.current.id) === 1 && (
<Wizard.Stepper.Panel>
<PasoBasicosForm wizard={wizard} onChange={setWizard} />
</Wizard.Stepper.Panel>
)}
{Wizard.utils.getIndex(methods.current.id) === 2 && (
<Wizard.Stepper.Panel>
<PasoConfiguracionPanel
wizard={wizard}
onChange={setWizard}
onGenerarIA={simularGeneracionIA}
/>
</Wizard.Stepper.Panel>
)}
{Wizard.utils.getIndex(methods.current.id) === 3 && (
<Wizard.Stepper.Panel>
<PasoResumenCard wizard={wizard} />
</Wizard.Stepper.Panel>
)}
</div>
</div>
<Wizard.Stepper.Provider
initialStep={Wizard.utils.getFirst().id}
className="flex h-full flex-col"
>
{({ methods }) => {
const idx = Wizard.utils.getIndex(methods.current.id)
return (
<WizardLayout
title="Nueva Asignatura"
onClose={handleClose}
headerSlot={
<WizardResponsiveHeader wizard={Wizard} methods={methods} />
}
footerSlot={
<Wizard.Stepper.Controls>
<WizardControls
Wizard={Wizard}
methods={methods}
@@ -117,11 +112,41 @@ export function NuevaAsignaturaModalContainer({ planId }: { planId: string }) {
canContinueDesdeConfig={canContinueDesdeConfig}
onCreate={() => crearAsignatura(handleClose)}
/>
</>
)}
</Wizard.Stepper.Provider>
)}
</DialogContent>
</Dialog>
</Wizard.Stepper.Controls>
}
>
<div className="mx-auto max-w-3xl">
{idx === 0 && (
<Wizard.Stepper.Panel>
<PasoMetodoCardGroup wizard={wizard} onChange={setWizard} />
</Wizard.Stepper.Panel>
)}
{idx === 1 && (
<Wizard.Stepper.Panel>
<PasoBasicosForm wizard={wizard} onChange={setWizard} />
</Wizard.Stepper.Panel>
)}
{idx === 2 && (
<Wizard.Stepper.Panel>
<PasoConfiguracionPanel
wizard={wizard}
onChange={setWizard}
onGenerarIA={simularGeneracionIA}
/>
</Wizard.Stepper.Panel>
)}
{idx === 3 && (
<Wizard.Stepper.Panel>
<PasoResumenCard wizard={wizard} />
</Wizard.Stepper.Panel>
)}
</div>
</WizardLayout>
)
}}
</Wizard.Stepper.Provider>
)
}

View File

@@ -10,7 +10,6 @@ import { PasoDetallesPanel } from '@/components/planes/wizard/PasoDetallesPanel/
import { PasoModoCardGroup } from '@/components/planes/wizard/PasoModoCardGroup'
import { PasoResumenCard } from '@/components/planes/wizard/PasoResumenCard'
import { WizardControls } from '@/components/planes/wizard/WizardControls'
import { WizardHeader } from '@/components/planes/wizard/WizardHeader'
import { defineStepper } from '@/components/stepper'
import {
Card,
@@ -19,16 +18,12 @@ import {
CardHeader,
CardTitle,
} from '@/components/ui/card'
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { WizardLayout } from '@/components/wizard/WizardLayout'
import { WizardResponsiveHeader } from '@/components/wizard/WizardResponsiveHeader'
// import { useGeneratePlanAI } from '@/data/hooks/usePlans'
// Mock de permisos/rol
const auth_get_current_user_role = () => 'JEFE_CARRERA' as const
const auth_get_current_user_role = (): string => 'JEFE_CARRERA'
const Wizard = defineStepper(
{
@@ -64,136 +59,97 @@ export default function NuevoPlanModalContainer() {
// Crear plan: ahora la lógica vive en WizardControls
if (role !== 'JEFE_CARRERA') {
return (
<WizardLayout title="Nuevo plan de estudios" onClose={handleClose}>
<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>
</WizardLayout>
)
}
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"
<Wizard.Stepper.Provider
initialStep={Wizard.utils.getFirst().id}
className="flex h-full flex-col"
>
{({ methods }) => {
const idx = Wizard.utils.getIndex(methods.current.id)
return (
<WizardLayout
title="Nuevo plan de estudios"
onClose={handleClose}
headerSlot={
<WizardResponsiveHeader wizard={Wizard} methods={methods} />
}
footerSlot={
<Wizard.Stepper.Controls>
<WizardControls
errorMessage={wizard.errorMessage}
onPrev={() => methods.prev()}
onNext={() => methods.next()}
disablePrev={idx === 0 || wizard.isLoading}
disableNext={
wizard.isLoading ||
(idx === 0 && !canContinueDesdeModo) ||
(idx === 1 && !canContinueDesdeBasicos) ||
(idx === 2 && !canContinueDesdeDetalles)
}
disableCreate={wizard.isLoading}
isLastStep={idx >= Wizard.steps.length - 1}
wizard={wizard}
setWizard={setWizard}
/>
</Wizard.Stepper.Controls>
}
>
{({ methods }) => {
const currentIndex = Wizard.utils.getIndex(methods.current.id) + 1
const totalSteps = Wizard.steps.length
const nextStep = Wizard.steps[currentIndex] ?? {
title: '',
description: '',
}
return (
<>
<WizardHeader
currentIndex={currentIndex}
totalSteps={totalSteps}
currentTitle={methods.current.title}
currentDescription={methods.current.description}
nextTitle={nextStep.title}
onClose={handleClose}
Wizard={Wizard}
<div className="mx-auto max-w-3xl">
{idx === 0 && (
<Wizard.Stepper.Panel>
<PasoModoCardGroup wizard={wizard} onChange={setWizard} />
</Wizard.Stepper.Panel>
)}
{idx === 1 && (
<Wizard.Stepper.Panel>
<PasoBasicosForm wizard={wizard} onChange={setWizard} />
</Wizard.Stepper.Panel>
)}
{idx === 2 && (
<Wizard.Stepper.Panel>
<PasoDetallesPanel
wizard={wizard}
onChange={setWizard}
isLoading={wizard.isLoading}
/>
<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}
/>
</Wizard.Stepper.Panel>
)}
{Wizard.utils.getIndex(methods.current.id) === 2 && (
<Wizard.Stepper.Panel>
<PasoDetallesPanel
wizard={wizard}
onChange={setWizard}
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()}
disablePrev={
Wizard.utils.getIndex(methods.current.id) === 0 ||
wizard.isLoading
}
disableNext={
wizard.isLoading ||
(Wizard.utils.getIndex(methods.current.id) === 0 &&
!canContinueDesdeModo) ||
(Wizard.utils.getIndex(methods.current.id) === 1 &&
!canContinueDesdeBasicos) ||
(Wizard.utils.getIndex(methods.current.id) === 2 &&
!canContinueDesdeDetalles)
}
disableCreate={wizard.isLoading}
isLastStep={
Wizard.utils.getIndex(methods.current.id) >=
Wizard.steps.length - 1
}
wizard={wizard}
setWizard={setWizard}
/>
</Wizard.Stepper.Controls>
</div>
</>
)
}}
</Wizard.Stepper.Provider>
)}
</DialogContent>
</Dialog>
</Wizard.Stepper.Panel>
)}
{idx === 3 && (
<Wizard.Stepper.Panel>
<PasoResumenCard wizard={wizard} />
</Wizard.Stepper.Panel>
)}
</div>
</WizardLayout>
)
}}
</Wizard.Stepper.Provider>
)
}

View File

@@ -8,8 +8,8 @@ export function useNuevoPlanWizard() {
tipoOrigen: null,
datosBasicos: {
nombrePlan: '',
carreraId: '',
facultadId: '',
facultad: { id: '', nombre: '' },
carrera: { id: '', nombre: '' },
nivel: '',
tipoCiclo: '',
numCiclos: undefined,
@@ -53,8 +53,8 @@ export function useNuevoPlanWizard() {
const canContinueDesdeBasicos =
!!wizard.datosBasicos.nombrePlan &&
!!wizard.datosBasicos.carreraId &&
!!wizard.datosBasicos.facultadId &&
!!wizard.datosBasicos.carrera.id &&
!!wizard.datosBasicos.facultad.id &&
!!wizard.datosBasicos.nivel &&
wizard.datosBasicos.numCiclos !== undefined &&
wizard.datosBasicos.numCiclos > 0 &&

View File

@@ -19,8 +19,14 @@ export type NewPlanWizardState = {
tipoOrigen: TipoOrigen | null
datosBasicos: {
nombrePlan: string
carreraId: string
facultadId: string
facultad: {
id: string
nombre: string
}
carrera: {
id: string
nombre: string
}
nivel: NivelPlanEstudio | ''
tipoCiclo: TipoCiclo | ''
numCiclos: number | undefined

View File

@@ -12,21 +12,19 @@ import { Route as rootRouteImport } from './routes/__root'
import { Route as LoginRouteImport } from './routes/login'
import { Route as DashboardRouteImport } from './routes/dashboard'
import { Route as IndexRouteImport } from './routes/index'
import { Route as PlanesListaRouteImport } from './routes/planes/_lista'
import { Route as DemoTanstackQueryRouteImport } from './routes/demo/tanstack-query'
import { Route as PlanesListaRouteRouteImport } from './routes/planes/_lista/route'
import { Route as PlanesListaNuevoRouteImport } from './routes/planes/_lista/nuevo'
import { Route as PlanesPlanIdAsignaturasRouteRouteImport } from './routes/planes/$planId/asignaturas/route'
import { Route as PlanesPlanIdDetalleRouteRouteImport } from './routes/planes/$planId/_detalle/route'
import { Route as PlanesPlanIdDetalleRouteImport } from './routes/planes/$planId/_detalle'
import { Route as PlanesPlanIdDetalleIndexRouteImport } from './routes/planes/$planId/_detalle/index'
import { Route as PlanesPlanIdAsignaturasAsignaturaIdRouteImport } from './routes/planes/$planId/asignaturas/$asignaturaId'
import { Route as PlanesPlanIdDetalleMapaRouteImport } from './routes/planes/$planId/_detalle/mapa'
import { Route as PlanesPlanIdDetalleIaplanRouteImport } from './routes/planes/$planId/_detalle/iaplan'
import { Route as PlanesPlanIdDetalleHistorialRouteImport } from './routes/planes/$planId/_detalle/historial'
import { Route as PlanesPlanIdDetalleFlujoRouteImport } from './routes/planes/$planId/_detalle/flujo'
import { Route as PlanesPlanIdDetalleDocumentoRouteImport } from './routes/planes/$planId/_detalle/documento'
import { Route as PlanesPlanIdDetalleAsignaturasRouteImport } from './routes/planes/$planId/_detalle/asignaturas'
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'
import { Route as PlanesPlanIdDetalleAsignaturasIndexRouteImport } from './routes/planes/$planId/_detalle/asignaturas/index'
import { Route as PlanesPlanIdDetalleAsignaturasNuevaRouteImport } from './routes/planes/$planId/_detalle/asignaturas/nueva'
const LoginRoute = LoginRouteImport.update({
id: '/login',
@@ -43,147 +41,133 @@ const IndexRoute = IndexRouteImport.update({
path: '/',
getParentRoute: () => rootRouteImport,
} as any)
const PlanesListaRoute = PlanesListaRouteImport.update({
id: '/planes/_lista',
path: '/planes',
getParentRoute: () => rootRouteImport,
} as any)
const DemoTanstackQueryRoute = DemoTanstackQueryRouteImport.update({
id: '/demo/tanstack-query',
path: '/demo/tanstack-query',
getParentRoute: () => rootRouteImport,
} as any)
const PlanesListaRouteRoute = PlanesListaRouteRouteImport.update({
id: '/planes/_lista',
path: '/planes',
getParentRoute: () => rootRouteImport,
} as any)
const PlanesListaNuevoRoute = PlanesListaNuevoRouteImport.update({
id: '/nuevo',
path: '/nuevo',
getParentRoute: () => PlanesListaRouteRoute,
getParentRoute: () => PlanesListaRoute,
} as any)
const PlanesPlanIdDetalleRoute = PlanesPlanIdDetalleRouteImport.update({
id: '/planes/$planId/_detalle',
path: '/planes/$planId',
getParentRoute: () => rootRouteImport,
} as any)
const PlanesPlanIdAsignaturasRouteRoute =
PlanesPlanIdAsignaturasRouteRouteImport.update({
id: '/planes/$planId/asignaturas',
path: '/planes/$planId/asignaturas',
getParentRoute: () => rootRouteImport,
} as any)
const PlanesPlanIdDetalleRouteRoute =
PlanesPlanIdDetalleRouteRouteImport.update({
id: '/planes/$planId/_detalle',
path: '/planes/$planId',
getParentRoute: () => rootRouteImport,
} as any)
const PlanesPlanIdDetalleIndexRoute =
PlanesPlanIdDetalleIndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => PlanesPlanIdDetalleRouteRoute,
getParentRoute: () => PlanesPlanIdDetalleRoute,
} as any)
const PlanesPlanIdAsignaturasAsignaturaIdRoute =
PlanesPlanIdAsignaturasAsignaturaIdRouteImport.update({
id: '/planes/$planId/asignaturas/$asignaturaId',
path: '/planes/$planId/asignaturas/$asignaturaId',
getParentRoute: () => rootRouteImport,
} as any)
const PlanesPlanIdDetalleMapaRoute = PlanesPlanIdDetalleMapaRouteImport.update({
id: '/mapa',
path: '/mapa',
getParentRoute: () => PlanesPlanIdDetalleRouteRoute,
getParentRoute: () => PlanesPlanIdDetalleRoute,
} as any)
const PlanesPlanIdDetalleIaplanRoute =
PlanesPlanIdDetalleIaplanRouteImport.update({
id: '/iaplan',
path: '/iaplan',
getParentRoute: () => PlanesPlanIdDetalleRouteRoute,
getParentRoute: () => PlanesPlanIdDetalleRoute,
} as any)
const PlanesPlanIdDetalleHistorialRoute =
PlanesPlanIdDetalleHistorialRouteImport.update({
id: '/historial',
path: '/historial',
getParentRoute: () => PlanesPlanIdDetalleRouteRoute,
getParentRoute: () => PlanesPlanIdDetalleRoute,
} as any)
const PlanesPlanIdDetalleFlujoRoute =
PlanesPlanIdDetalleFlujoRouteImport.update({
id: '/flujo',
path: '/flujo',
getParentRoute: () => PlanesPlanIdDetalleRouteRoute,
getParentRoute: () => PlanesPlanIdDetalleRoute,
} as any)
const PlanesPlanIdDetalleDocumentoRoute =
PlanesPlanIdDetalleDocumentoRouteImport.update({
id: '/documento',
path: '/documento',
getParentRoute: () => PlanesPlanIdDetalleRouteRoute,
getParentRoute: () => PlanesPlanIdDetalleRoute,
} as any)
const PlanesPlanIdDetalleAsignaturasRoute =
PlanesPlanIdDetalleAsignaturasRouteImport.update({
id: '/asignaturas',
path: '/asignaturas',
getParentRoute: () => PlanesPlanIdDetalleRouteRoute,
const PlanesPlanIdDetalleAsignaturasIndexRoute =
PlanesPlanIdDetalleAsignaturasIndexRouteImport.update({
id: '/asignaturas/',
path: '/asignaturas/',
getParentRoute: () => PlanesPlanIdDetalleRoute,
} as any)
const PlanesPlanIdAsignaturasListaRouteRoute =
PlanesPlanIdAsignaturasListaRouteRouteImport.update({
id: '/_lista',
getParentRoute: () => PlanesPlanIdAsignaturasRouteRoute,
} as any)
const PlanesPlanIdAsignaturasAsignaturaIdRouteRoute =
PlanesPlanIdAsignaturasAsignaturaIdRouteRouteImport.update({
id: '/$asignaturaId',
path: '/$asignaturaId',
getParentRoute: () => PlanesPlanIdAsignaturasRouteRoute,
} as any)
const PlanesPlanIdAsignaturasListaNuevaRoute =
PlanesPlanIdAsignaturasListaNuevaRouteImport.update({
id: '/nueva',
path: '/nueva',
getParentRoute: () => PlanesPlanIdAsignaturasListaRouteRoute,
const PlanesPlanIdDetalleAsignaturasNuevaRoute =
PlanesPlanIdDetalleAsignaturasNuevaRouteImport.update({
id: '/asignaturas/nueva',
path: '/asignaturas/nueva',
getParentRoute: () => PlanesPlanIdDetalleRoute,
} as any)
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
'/dashboard': typeof DashboardRoute
'/login': typeof LoginRoute
'/planes': typeof PlanesListaRouteRouteWithChildren
'/demo/tanstack-query': typeof DemoTanstackQueryRoute
'/planes/$planId': typeof PlanesPlanIdDetalleRouteRouteWithChildren
'/planes/$planId/asignaturas': typeof PlanesPlanIdDetalleAsignaturasRoute
'/planes': typeof PlanesListaRouteWithChildren
'/planes/$planId': typeof PlanesPlanIdDetalleRouteWithChildren
'/planes/nuevo': typeof PlanesListaNuevoRoute
'/planes/$planId/asignaturas/$asignaturaId': typeof PlanesPlanIdAsignaturasAsignaturaIdRouteRoute
'/planes/$planId/documento': typeof PlanesPlanIdDetalleDocumentoRoute
'/planes/$planId/flujo': typeof PlanesPlanIdDetalleFlujoRoute
'/planes/$planId/historial': typeof PlanesPlanIdDetalleHistorialRoute
'/planes/$planId/iaplan': typeof PlanesPlanIdDetalleIaplanRoute
'/planes/$planId/mapa': typeof PlanesPlanIdDetalleMapaRoute
'/planes/$planId/asignaturas/$asignaturaId': typeof PlanesPlanIdAsignaturasAsignaturaIdRoute
'/planes/$planId/': typeof PlanesPlanIdDetalleIndexRoute
'/planes/$planId/asignaturas/nueva': typeof PlanesPlanIdAsignaturasListaNuevaRoute
'/planes/$planId/asignaturas/nueva': typeof PlanesPlanIdDetalleAsignaturasNuevaRoute
'/planes/$planId/asignaturas/': typeof PlanesPlanIdDetalleAsignaturasIndexRoute
}
export interface FileRoutesByTo {
'/': typeof IndexRoute
'/dashboard': typeof DashboardRoute
'/login': typeof LoginRoute
'/planes': typeof PlanesListaRouteRouteWithChildren
'/demo/tanstack-query': typeof DemoTanstackQueryRoute
'/planes/$planId/asignaturas': typeof PlanesPlanIdDetalleAsignaturasRoute
'/planes': typeof PlanesListaRouteWithChildren
'/planes/nuevo': typeof PlanesListaNuevoRoute
'/planes/$planId/asignaturas/$asignaturaId': typeof PlanesPlanIdAsignaturasAsignaturaIdRouteRoute
'/planes/$planId/documento': typeof PlanesPlanIdDetalleDocumentoRoute
'/planes/$planId/flujo': typeof PlanesPlanIdDetalleFlujoRoute
'/planes/$planId/historial': typeof PlanesPlanIdDetalleHistorialRoute
'/planes/$planId/iaplan': typeof PlanesPlanIdDetalleIaplanRoute
'/planes/$planId/mapa': typeof PlanesPlanIdDetalleMapaRoute
'/planes/$planId/asignaturas/$asignaturaId': typeof PlanesPlanIdAsignaturasAsignaturaIdRoute
'/planes/$planId': typeof PlanesPlanIdDetalleIndexRoute
'/planes/$planId/asignaturas/nueva': typeof PlanesPlanIdAsignaturasListaNuevaRoute
'/planes/$planId/asignaturas/nueva': typeof PlanesPlanIdDetalleAsignaturasNuevaRoute
'/planes/$planId/asignaturas': typeof PlanesPlanIdDetalleAsignaturasIndexRoute
}
export interface FileRoutesById {
__root__: typeof rootRouteImport
'/': typeof IndexRoute
'/dashboard': typeof DashboardRoute
'/login': typeof LoginRoute
'/planes/_lista': typeof PlanesListaRouteRouteWithChildren
'/demo/tanstack-query': typeof DemoTanstackQueryRoute
'/planes/$planId/_detalle': typeof PlanesPlanIdDetalleRouteRouteWithChildren
'/planes/$planId/asignaturas': typeof PlanesPlanIdAsignaturasRouteRouteWithChildren
'/planes/_lista': typeof PlanesListaRouteWithChildren
'/planes/$planId/_detalle': typeof PlanesPlanIdDetalleRouteWithChildren
'/planes/_lista/nuevo': typeof PlanesListaNuevoRoute
'/planes/$planId/asignaturas/$asignaturaId': typeof PlanesPlanIdAsignaturasAsignaturaIdRouteRoute
'/planes/$planId/asignaturas/_lista': typeof PlanesPlanIdAsignaturasListaRouteRouteWithChildren
'/planes/$planId/_detalle/asignaturas': typeof PlanesPlanIdDetalleAsignaturasRoute
'/planes/$planId/_detalle/documento': typeof PlanesPlanIdDetalleDocumentoRoute
'/planes/$planId/_detalle/flujo': typeof PlanesPlanIdDetalleFlujoRoute
'/planes/$planId/_detalle/historial': typeof PlanesPlanIdDetalleHistorialRoute
'/planes/$planId/_detalle/iaplan': typeof PlanesPlanIdDetalleIaplanRoute
'/planes/$planId/_detalle/mapa': typeof PlanesPlanIdDetalleMapaRoute
'/planes/$planId/asignaturas/$asignaturaId': typeof PlanesPlanIdAsignaturasAsignaturaIdRoute
'/planes/$planId/_detalle/': typeof PlanesPlanIdDetalleIndexRoute
'/planes/$planId/asignaturas/_lista/nueva': typeof PlanesPlanIdAsignaturasListaNuevaRoute
'/planes/$planId/_detalle/asignaturas/nueva': typeof PlanesPlanIdDetalleAsignaturasNuevaRoute
'/planes/$planId/_detalle/asignaturas/': typeof PlanesPlanIdDetalleAsignaturasIndexRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
@@ -191,66 +175,64 @@ export interface FileRouteTypes {
| '/'
| '/dashboard'
| '/login'
| '/planes'
| '/demo/tanstack-query'
| '/planes'
| '/planes/$planId'
| '/planes/$planId/asignaturas'
| '/planes/nuevo'
| '/planes/$planId/asignaturas/$asignaturaId'
| '/planes/$planId/documento'
| '/planes/$planId/flujo'
| '/planes/$planId/historial'
| '/planes/$planId/iaplan'
| '/planes/$planId/mapa'
| '/planes/$planId/asignaturas/$asignaturaId'
| '/planes/$planId/'
| '/planes/$planId/asignaturas/nueva'
| '/planes/$planId/asignaturas/'
fileRoutesByTo: FileRoutesByTo
to:
| '/'
| '/dashboard'
| '/login'
| '/planes'
| '/demo/tanstack-query'
| '/planes/$planId/asignaturas'
| '/planes'
| '/planes/nuevo'
| '/planes/$planId/asignaturas/$asignaturaId'
| '/planes/$planId/documento'
| '/planes/$planId/flujo'
| '/planes/$planId/historial'
| '/planes/$planId/iaplan'
| '/planes/$planId/mapa'
| '/planes/$planId/asignaturas/$asignaturaId'
| '/planes/$planId'
| '/planes/$planId/asignaturas/nueva'
| '/planes/$planId/asignaturas'
id:
| '__root__'
| '/'
| '/dashboard'
| '/login'
| '/planes/_lista'
| '/demo/tanstack-query'
| '/planes/_lista'
| '/planes/$planId/_detalle'
| '/planes/$planId/asignaturas'
| '/planes/_lista/nuevo'
| '/planes/$planId/asignaturas/$asignaturaId'
| '/planes/$planId/asignaturas/_lista'
| '/planes/$planId/_detalle/asignaturas'
| '/planes/$planId/_detalle/documento'
| '/planes/$planId/_detalle/flujo'
| '/planes/$planId/_detalle/historial'
| '/planes/$planId/_detalle/iaplan'
| '/planes/$planId/_detalle/mapa'
| '/planes/$planId/asignaturas/$asignaturaId'
| '/planes/$planId/_detalle/'
| '/planes/$planId/asignaturas/_lista/nueva'
| '/planes/$planId/_detalle/asignaturas/nueva'
| '/planes/$planId/_detalle/asignaturas/'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
DashboardRoute: typeof DashboardRoute
LoginRoute: typeof LoginRoute
PlanesListaRouteRoute: typeof PlanesListaRouteRouteWithChildren
DemoTanstackQueryRoute: typeof DemoTanstackQueryRoute
PlanesPlanIdDetalleRouteRoute: typeof PlanesPlanIdDetalleRouteRouteWithChildren
PlanesPlanIdAsignaturasRouteRoute: typeof PlanesPlanIdAsignaturasRouteRouteWithChildren
PlanesListaRoute: typeof PlanesListaRouteWithChildren
PlanesPlanIdDetalleRoute: typeof PlanesPlanIdDetalleRouteWithChildren
PlanesPlanIdAsignaturasAsignaturaIdRoute: typeof PlanesPlanIdAsignaturasAsignaturaIdRoute
}
declare module '@tanstack/react-router' {
@@ -276,6 +258,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport
}
'/planes/_lista': {
id: '/planes/_lista'
path: '/planes'
fullPath: '/planes'
preLoaderRoute: typeof PlanesListaRouteImport
parentRoute: typeof rootRouteImport
}
'/demo/tanstack-query': {
id: '/demo/tanstack-query'
path: '/demo/tanstack-query'
@@ -283,32 +272,18 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof DemoTanstackQueryRouteImport
parentRoute: typeof rootRouteImport
}
'/planes/_lista': {
id: '/planes/_lista'
path: '/planes'
fullPath: '/planes'
preLoaderRoute: typeof PlanesListaRouteRouteImport
parentRoute: typeof rootRouteImport
}
'/planes/_lista/nuevo': {
id: '/planes/_lista/nuevo'
path: '/nuevo'
fullPath: '/planes/nuevo'
preLoaderRoute: typeof PlanesListaNuevoRouteImport
parentRoute: typeof PlanesListaRouteRoute
}
'/planes/$planId/asignaturas': {
id: '/planes/$planId/asignaturas'
path: '/planes/$planId/asignaturas'
fullPath: '/planes/$planId/asignaturas'
preLoaderRoute: typeof PlanesPlanIdAsignaturasRouteRouteImport
parentRoute: typeof rootRouteImport
parentRoute: typeof PlanesListaRoute
}
'/planes/$planId/_detalle': {
id: '/planes/$planId/_detalle'
path: '/planes/$planId'
fullPath: '/planes/$planId'
preLoaderRoute: typeof PlanesPlanIdDetalleRouteRouteImport
preLoaderRoute: typeof PlanesPlanIdDetalleRouteImport
parentRoute: typeof rootRouteImport
}
'/planes/$planId/_detalle/': {
@@ -316,153 +291,115 @@ declare module '@tanstack/react-router' {
path: '/'
fullPath: '/planes/$planId/'
preLoaderRoute: typeof PlanesPlanIdDetalleIndexRouteImport
parentRoute: typeof PlanesPlanIdDetalleRouteRoute
parentRoute: typeof PlanesPlanIdDetalleRoute
}
'/planes/$planId/asignaturas/$asignaturaId': {
id: '/planes/$planId/asignaturas/$asignaturaId'
path: '/planes/$planId/asignaturas/$asignaturaId'
fullPath: '/planes/$planId/asignaturas/$asignaturaId'
preLoaderRoute: typeof PlanesPlanIdAsignaturasAsignaturaIdRouteImport
parentRoute: typeof rootRouteImport
}
'/planes/$planId/_detalle/mapa': {
id: '/planes/$planId/_detalle/mapa'
path: '/mapa'
fullPath: '/planes/$planId/mapa'
preLoaderRoute: typeof PlanesPlanIdDetalleMapaRouteImport
parentRoute: typeof PlanesPlanIdDetalleRouteRoute
parentRoute: typeof PlanesPlanIdDetalleRoute
}
'/planes/$planId/_detalle/iaplan': {
id: '/planes/$planId/_detalle/iaplan'
path: '/iaplan'
fullPath: '/planes/$planId/iaplan'
preLoaderRoute: typeof PlanesPlanIdDetalleIaplanRouteImport
parentRoute: typeof PlanesPlanIdDetalleRouteRoute
parentRoute: typeof PlanesPlanIdDetalleRoute
}
'/planes/$planId/_detalle/historial': {
id: '/planes/$planId/_detalle/historial'
path: '/historial'
fullPath: '/planes/$planId/historial'
preLoaderRoute: typeof PlanesPlanIdDetalleHistorialRouteImport
parentRoute: typeof PlanesPlanIdDetalleRouteRoute
parentRoute: typeof PlanesPlanIdDetalleRoute
}
'/planes/$planId/_detalle/flujo': {
id: '/planes/$planId/_detalle/flujo'
path: '/flujo'
fullPath: '/planes/$planId/flujo'
preLoaderRoute: typeof PlanesPlanIdDetalleFlujoRouteImport
parentRoute: typeof PlanesPlanIdDetalleRouteRoute
parentRoute: typeof PlanesPlanIdDetalleRoute
}
'/planes/$planId/_detalle/documento': {
id: '/planes/$planId/_detalle/documento'
path: '/documento'
fullPath: '/planes/$planId/documento'
preLoaderRoute: typeof PlanesPlanIdDetalleDocumentoRouteImport
parentRoute: typeof PlanesPlanIdDetalleRouteRoute
parentRoute: typeof PlanesPlanIdDetalleRoute
}
'/planes/$planId/_detalle/asignaturas': {
id: '/planes/$planId/_detalle/asignaturas'
'/planes/$planId/_detalle/asignaturas/': {
id: '/planes/$planId/_detalle/asignaturas/'
path: '/asignaturas'
fullPath: '/planes/$planId/asignaturas'
preLoaderRoute: typeof PlanesPlanIdDetalleAsignaturasRouteImport
parentRoute: typeof PlanesPlanIdDetalleRouteRoute
fullPath: '/planes/$planId/asignaturas/'
preLoaderRoute: typeof PlanesPlanIdDetalleAsignaturasIndexRouteImport
parentRoute: typeof PlanesPlanIdDetalleRoute
}
'/planes/$planId/asignaturas/_lista': {
id: '/planes/$planId/asignaturas/_lista'
path: ''
fullPath: '/planes/$planId/asignaturas'
preLoaderRoute: typeof PlanesPlanIdAsignaturasListaRouteRouteImport
parentRoute: typeof PlanesPlanIdAsignaturasRouteRoute
}
'/planes/$planId/asignaturas/$asignaturaId': {
id: '/planes/$planId/asignaturas/$asignaturaId'
path: '/$asignaturaId'
fullPath: '/planes/$planId/asignaturas/$asignaturaId'
preLoaderRoute: typeof PlanesPlanIdAsignaturasAsignaturaIdRouteRouteImport
parentRoute: typeof PlanesPlanIdAsignaturasRouteRoute
}
'/planes/$planId/asignaturas/_lista/nueva': {
id: '/planes/$planId/asignaturas/_lista/nueva'
path: '/nueva'
'/planes/$planId/_detalle/asignaturas/nueva': {
id: '/planes/$planId/_detalle/asignaturas/nueva'
path: '/asignaturas/nueva'
fullPath: '/planes/$planId/asignaturas/nueva'
preLoaderRoute: typeof PlanesPlanIdAsignaturasListaNuevaRouteImport
parentRoute: typeof PlanesPlanIdAsignaturasListaRouteRoute
preLoaderRoute: typeof PlanesPlanIdDetalleAsignaturasNuevaRouteImport
parentRoute: typeof PlanesPlanIdDetalleRoute
}
}
}
interface PlanesListaRouteRouteChildren {
interface PlanesListaRouteChildren {
PlanesListaNuevoRoute: typeof PlanesListaNuevoRoute
}
const PlanesListaRouteRouteChildren: PlanesListaRouteRouteChildren = {
const PlanesListaRouteChildren: PlanesListaRouteChildren = {
PlanesListaNuevoRoute: PlanesListaNuevoRoute,
}
const PlanesListaRouteRouteWithChildren =
PlanesListaRouteRoute._addFileChildren(PlanesListaRouteRouteChildren)
const PlanesListaRouteWithChildren = PlanesListaRoute._addFileChildren(
PlanesListaRouteChildren,
)
interface PlanesPlanIdDetalleRouteRouteChildren {
PlanesPlanIdDetalleAsignaturasRoute: typeof PlanesPlanIdDetalleAsignaturasRoute
interface PlanesPlanIdDetalleRouteChildren {
PlanesPlanIdDetalleDocumentoRoute: typeof PlanesPlanIdDetalleDocumentoRoute
PlanesPlanIdDetalleFlujoRoute: typeof PlanesPlanIdDetalleFlujoRoute
PlanesPlanIdDetalleHistorialRoute: typeof PlanesPlanIdDetalleHistorialRoute
PlanesPlanIdDetalleIaplanRoute: typeof PlanesPlanIdDetalleIaplanRoute
PlanesPlanIdDetalleMapaRoute: typeof PlanesPlanIdDetalleMapaRoute
PlanesPlanIdDetalleIndexRoute: typeof PlanesPlanIdDetalleIndexRoute
PlanesPlanIdDetalleAsignaturasNuevaRoute: typeof PlanesPlanIdDetalleAsignaturasNuevaRoute
PlanesPlanIdDetalleAsignaturasIndexRoute: typeof PlanesPlanIdDetalleAsignaturasIndexRoute
}
const PlanesPlanIdDetalleRouteRouteChildren: PlanesPlanIdDetalleRouteRouteChildren =
{
PlanesPlanIdDetalleAsignaturasRoute: PlanesPlanIdDetalleAsignaturasRoute,
PlanesPlanIdDetalleDocumentoRoute: PlanesPlanIdDetalleDocumentoRoute,
PlanesPlanIdDetalleFlujoRoute: PlanesPlanIdDetalleFlujoRoute,
PlanesPlanIdDetalleHistorialRoute: PlanesPlanIdDetalleHistorialRoute,
PlanesPlanIdDetalleIaplanRoute: PlanesPlanIdDetalleIaplanRoute,
PlanesPlanIdDetalleMapaRoute: PlanesPlanIdDetalleMapaRoute,
PlanesPlanIdDetalleIndexRoute: PlanesPlanIdDetalleIndexRoute,
}
const PlanesPlanIdDetalleRouteRouteWithChildren =
PlanesPlanIdDetalleRouteRoute._addFileChildren(
PlanesPlanIdDetalleRouteRouteChildren,
)
interface PlanesPlanIdAsignaturasListaRouteRouteChildren {
PlanesPlanIdAsignaturasListaNuevaRoute: typeof PlanesPlanIdAsignaturasListaNuevaRoute
const PlanesPlanIdDetalleRouteChildren: PlanesPlanIdDetalleRouteChildren = {
PlanesPlanIdDetalleDocumentoRoute: PlanesPlanIdDetalleDocumentoRoute,
PlanesPlanIdDetalleFlujoRoute: PlanesPlanIdDetalleFlujoRoute,
PlanesPlanIdDetalleHistorialRoute: PlanesPlanIdDetalleHistorialRoute,
PlanesPlanIdDetalleIaplanRoute: PlanesPlanIdDetalleIaplanRoute,
PlanesPlanIdDetalleMapaRoute: PlanesPlanIdDetalleMapaRoute,
PlanesPlanIdDetalleIndexRoute: PlanesPlanIdDetalleIndexRoute,
PlanesPlanIdDetalleAsignaturasNuevaRoute:
PlanesPlanIdDetalleAsignaturasNuevaRoute,
PlanesPlanIdDetalleAsignaturasIndexRoute:
PlanesPlanIdDetalleAsignaturasIndexRoute,
}
const PlanesPlanIdAsignaturasListaRouteRouteChildren: PlanesPlanIdAsignaturasListaRouteRouteChildren =
{
PlanesPlanIdAsignaturasListaNuevaRoute:
PlanesPlanIdAsignaturasListaNuevaRoute,
}
const PlanesPlanIdAsignaturasListaRouteRouteWithChildren =
PlanesPlanIdAsignaturasListaRouteRoute._addFileChildren(
PlanesPlanIdAsignaturasListaRouteRouteChildren,
)
interface PlanesPlanIdAsignaturasRouteRouteChildren {
PlanesPlanIdAsignaturasAsignaturaIdRouteRoute: typeof PlanesPlanIdAsignaturasAsignaturaIdRouteRoute
PlanesPlanIdAsignaturasListaRouteRoute: typeof PlanesPlanIdAsignaturasListaRouteRouteWithChildren
}
const PlanesPlanIdAsignaturasRouteRouteChildren: PlanesPlanIdAsignaturasRouteRouteChildren =
{
PlanesPlanIdAsignaturasAsignaturaIdRouteRoute:
PlanesPlanIdAsignaturasAsignaturaIdRouteRoute,
PlanesPlanIdAsignaturasListaRouteRoute:
PlanesPlanIdAsignaturasListaRouteRouteWithChildren,
}
const PlanesPlanIdAsignaturasRouteRouteWithChildren =
PlanesPlanIdAsignaturasRouteRoute._addFileChildren(
PlanesPlanIdAsignaturasRouteRouteChildren,
)
const PlanesPlanIdDetalleRouteWithChildren =
PlanesPlanIdDetalleRoute._addFileChildren(PlanesPlanIdDetalleRouteChildren)
const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
DashboardRoute: DashboardRoute,
LoginRoute: LoginRoute,
PlanesListaRouteRoute: PlanesListaRouteRouteWithChildren,
DemoTanstackQueryRoute: DemoTanstackQueryRoute,
PlanesPlanIdDetalleRouteRoute: PlanesPlanIdDetalleRouteRouteWithChildren,
PlanesPlanIdAsignaturasRouteRoute:
PlanesPlanIdAsignaturasRouteRouteWithChildren,
PlanesListaRoute: PlanesListaRouteWithChildren,
PlanesPlanIdDetalleRoute: PlanesPlanIdDetalleRouteWithChildren,
PlanesPlanIdAsignaturasAsignaturaIdRoute:
PlanesPlanIdAsignaturasAsignaturaIdRoute,
}
export const routeTree = rootRouteImport
._addFileChildren(rootRouteChildren)

View File

@@ -35,6 +35,8 @@ export const Route = createFileRoute('/planes/$planId/_detalle')({
} catch (e: any) {
// PGRST116: The result contains 0 rows
if (e?.code === 'PGRST116') {
console.log('not found on', Route.path)
throw notFound()
}
throw e

View File

@@ -10,7 +10,7 @@ import {
} from 'lucide-react'
import { useState, useMemo } from 'react'
import type { Asignatura } from '@/types/plan'
import type { Asignatura, AsignaturaStatus, TipoAsignatura } from '@/types/plan'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
@@ -33,17 +33,24 @@ import {
import { usePlanAsignaturas, usePlanLineas } from '@/data'
// --- Configuración de Estilos ---
const statusConfig: Record<string, { label: string; className: string }> = {
const statusConfig: Record<
AsignaturaStatus,
{ label: string; className: string }
> = {
borrador: { label: 'Borrador', className: 'bg-slate-100 text-slate-600' },
revisada: { label: 'Revisada', className: 'bg-amber-100 text-amber-700' },
aprobada: { label: 'Aprobada', className: 'bg-emerald-100 text-emerald-700' },
}
const tipoConfig: Record<string, { label: string; className: string }> = {
obligatoria: { label: 'Obligatoria', className: 'bg-blue-100 text-blue-700' },
optativa: { label: 'Optativa', className: 'bg-purple-100 text-purple-700' },
troncal: { label: 'Troncal', className: 'bg-slate-100 text-slate-700' },
}
const tipoConfig: Record<TipoAsignatura, { label: string; className: string }> =
{
obligatoria: {
label: 'Obligatoria',
className: 'bg-blue-100 text-blue-700',
},
optativa: { label: 'Optativa', className: 'bg-purple-100 text-purple-700' },
troncal: { label: 'Troncal', className: 'bg-slate-100 text-slate-700' },
}
// --- Mapeadores de API ---
const mapAsignaturas = (asigApi: Array<any> = []): Array<Asignatura> => {
@@ -59,10 +66,13 @@ const mapAsignaturas = (asigApi: Array<any> = []): Array<Asignatura> => {
estado: 'borrador', // O el campo que venga de tu API
hd: Math.floor((asig.horas_semana ?? 0) / 2),
hi: Math.ceil((asig.horas_semana ?? 0) / 2),
prerrequisitos: Array.isArray(asig.prerrequisitos)
? asig.prerrequisitos
: [],
}))
}
export const Route = createFileRoute('/planes/$planId/_detalle/asignaturas')({
export const Route = createFileRoute('/planes/$planId/_detalle/asignaturas/')({
component: AsignaturasPage,
})
@@ -131,7 +141,17 @@ function AsignaturasPage() {
<Button variant="outline" size="sm">
<Copy className="mr-2 h-4 w-4" /> Clonar
</Button>
<Button className="bg-emerald-700 hover:bg-emerald-800">
<Button
onClick={() => {
console.log('planId desde asignaturas', planId)
navigate({
to: `/planes/${planId}/asignaturas/nueva`,
resetScroll: false,
})
}}
className="ring-offset-background bg-primary text-primary-foreground hover:bg-primary/90 inline-flex h-11 items-center justify-center gap-2 rounded-md px-8 text-sm font-medium shadow-md transition-colors"
>
<Plus className="mr-2 h-4 w-4" /> Nueva Asignatura
</Button>
</div>
@@ -262,17 +282,17 @@ function AsignaturasPage() {
<TableCell>
<Badge
variant="outline"
className={`capitalize shadow-sm ${tipoConfig[asignatura.tipo]?.className}`}
className={`capitalize shadow-sm ${tipoConfig[asignatura.tipo].className}`}
>
{tipoConfig[asignatura.tipo]?.label}
{tipoConfig[asignatura.tipo].label}
</Badge>
</TableCell>
<TableCell>
<Badge
variant="outline"
className={`capitalize shadow-sm ${statusConfig[asignatura.estado]?.className}`}
className={`capitalize shadow-sm ${statusConfig[asignatura.estado].className}`}
>
{statusConfig[asignatura.estado]?.label}
{statusConfig[asignatura.estado].label}
</Badge>
</TableCell>
<TableCell>

View File

@@ -3,12 +3,13 @@ import { createFileRoute } from '@tanstack/react-router'
import { NuevaAsignaturaModalContainer } from '@/features/asignaturas/nueva/NuevaAsignaturaModalContainer'
export const Route = createFileRoute(
'/planes/$planId/asignaturas/_lista/nueva',
'/planes/$planId/_detalle/asignaturas/nueva',
)({
component: NuevaAsignaturaModal,
})
function NuevaAsignaturaModal() {
const { planId } = Route.useParams()
console.log('planId desde nueva', planId)
return <NuevaAsignaturaModalContainer planId={planId} />
}

View File

@@ -0,0 +1,44 @@
import { createFileRoute, notFound } from '@tanstack/react-router'
import AsignaturaDetailPage from '@/components/asignaturas/detalle/AsignaturaDetailPage'
import { NotFoundPage } from '@/components/ui/NotFoundPage'
import { subjects_get } from '@/data/api/subjects.api'
import { qk } from '@/data/query/keys'
export const Route = createFileRoute(
'/planes/$planId/asignaturas/$asignaturaId',
)({
loader: async ({ context: { queryClient }, params: { asignaturaId } }) => {
try {
await queryClient.ensureQueryData({
queryKey: qk.asignatura(asignaturaId),
queryFn: () => subjects_get(asignaturaId),
})
} catch (e: any) {
// PGRST116: The result contains 0 rows (Supabase Single response error)
if (e?.code === 'PGRST116') {
throw notFound()
}
throw e
}
},
notFoundComponent: () => {
return (
<NotFoundPage
title="Materia no encontrada"
message="La asignatura que buscas no existe o fue eliminada."
/>
)
},
component: RouteComponent,
})
function RouteComponent() {
// const { planId, asignaturaId } = Route.useParams()
return (
<div>
<AsignaturaDetailPage></AsignaturaDetailPage>
</div>
)
}

View File

@@ -1,18 +0,0 @@
import AsignaturaDetailPage from '@/components/asignaturas/detalle/AsignaturaDetailPage'
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute(
'/planes/$planId/asignaturas/$asignaturaId',
)({
component: RouteComponent,
})
function RouteComponent() {
//const { planId, asignaturaId } = Route.useParams()
return (
<div>
<AsignaturaDetailPage></AsignaturaDetailPage>
</div>
)
}

View File

@@ -1,16 +0,0 @@
import { createFileRoute, Outlet } from '@tanstack/react-router'
export const Route = createFileRoute('/planes/$planId/asignaturas/_lista')({
component: RouteComponent,
})
function RouteComponent() {
return (
<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">
<h1 className="text-foreground text-2xl font-semibold">Asignaturas</h1>
<Outlet />
</div>
</main>
)
}

View File

@@ -1,34 +0,0 @@
import { createFileRoute, Outlet, notFound } from '@tanstack/react-router'
import { NotFoundPage } from '@/components/ui/NotFoundPage'
import { plans_get } from '@/data/api/plans.api'
import { qk } from '@/data/query/keys'
export const Route = createFileRoute('/planes/$planId/asignaturas')({
loader: async ({ context: { queryClient }, params: { planId } }) => {
try {
await queryClient.ensureQueryData({
queryKey: qk.plan(planId),
queryFn: () => plans_get(planId),
})
} catch (e: any) {
if (e?.code === 'PGRST116') {
throw notFound()
}
throw e
}
},
notFoundComponent: () => {
return (
<NotFoundPage
title="Plan de Estudios no encontrado"
message="El plan de estudios que intentas consultar no existe o no tienes permisos para verlo."
/>
)
},
component: AsignaturasLayout,
})
function AsignaturasLayout() {
return <Outlet />
}

View File

@@ -125,7 +125,11 @@ function RouteComponent() {
</div>
</div>
<button
onClick={() => navigate({ to: '/planes/nuevo' })}
onClick={() => {
console.log('planId')
navigate({ to: '/planes/nuevo', resetScroll: false })
}}
className="ring-offset-background bg-primary text-primary-foreground hover:bg-primary/90 inline-flex h-11 items-center justify-center gap-2 rounded-md px-8 text-sm font-medium shadow-md transition-colors"
>
<Icons.Plus /> Nuevo plan de estudios

File diff suppressed because it is too large Load Diff