6 Commits

24 changed files with 1888 additions and 1607 deletions

View File

@@ -442,20 +442,13 @@ function DatosGenerales({
? config.examples[0] ? config.examples[0]
: '' : ''
// 2. CONTENIDO REAL (Viene de data.datos -> valoresActuales)
// El problema: Si 'description' en 'datos' es igual a la de la 'estructura',
// el usuario aún no ha redactado nada real.
const valActual = valoresActuales[key] const valActual = valoresActuales[key]
// Lógica para determinar si mostrar el contenido o dejarlo vacío (para que salga el placeholder)
// Si el contenido en 'datos' es idéntico a la instrucción de la 'estructura',
// asumimos que no hay contenido real todavía.
const isContentEmpty = const isContentEmpty =
!valActual?.description || !valActual?.description ||
valActual.description === config.description valActual.description === config.description
const currentContent = valActual?.description ?? '' const currentContent = valActual ?? ''
return ( return (
<InfoCard <InfoCard

View File

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

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

@@ -165,7 +165,7 @@ export async function plan_asignaturas_list(
const { data, error } = await supabase const { data, error } = await supabase
.from('asignaturas') .from('asignaturas')
.select( .select(
'id,plan_estudio_id,estructura_id,facultad_propietaria_id,codigo,nombre,tipo,creditos,horas_semana,numero_ciclo,linea_plan_id,orden_celda,datos,contenido_tematico,tipo_origen,meta_origen,creado_por,actualizado_por,creado_en,actualizado_en', 'id,plan_estudio_id,horas_academicas, horas_independientes,estructura_id,codigo,nombre,tipo,creditos,numero_ciclo,linea_plan_id,orden_celda,datos,contenido_tematico,tipo_origen,meta_origen,creado_por,actualizado_por,creado_en,actualizado_en',
) )
.eq('plan_estudio_id', planId) .eq('plan_estudio_id', planId)
.order('numero_ciclo', { ascending: true, nullsFirst: false }) .order('numero_ciclo', { ascending: true, nullsFirst: false })

View File

@@ -1,6 +1,9 @@
import { supabaseBrowser } from '../supabase/client' import { supabaseBrowser } from '../supabase/client'
import { invokeEdge } from '../supabase/invokeEdge' import { invokeEdge } from '../supabase/invokeEdge'
import { throwIfError, requireData } from './_helpers' import { throwIfError, requireData } from './_helpers'
import type { DocumentoResult } from './plans.api'
import type { import type {
Asignatura, Asignatura,
BibliografiaAsignatura, BibliografiaAsignatura,
@@ -8,7 +11,6 @@ import type {
TipoAsignatura, TipoAsignatura,
UUID, UUID,
} from '../types/domain' } from '../types/domain'
import type { DocumentoResult } from './plans.api'
const EDGE = { const EDGE = {
subjects_create_manual: 'subjects_create_manual', subjects_create_manual: 'subjects_create_manual',
@@ -32,7 +34,7 @@ export async function subjects_get(subjectId: UUID): Promise<Asignatura> {
.from('asignaturas') .from('asignaturas')
.select( .select(
` `
id,plan_estudio_id,estructura_id,facultad_propietaria_id,codigo,nombre,tipo,creditos,horas_semana,numero_ciclo,linea_plan_id,orden_celda,datos,contenido_tematico,tipo_origen,meta_origen,creado_por,actualizado_por,creado_en,actualizado_en, id,plan_estudio_id,estructura_id,codigo,nombre,tipo,creditos,numero_ciclo,linea_plan_id,orden_celda,datos,contenido_tematico,tipo_origen,meta_origen,creado_por,actualizado_por,creado_en,actualizado_en,
planes_estudio( planes_estudio(
id,carrera_id,estructura_id,nombre,nivel,tipo_ciclo,numero_ciclos,datos,estado_actual_id,activo,tipo_origen,meta_origen,creado_por,actualizado_por,creado_en,actualizado_en, id,carrera_id,estructura_id,nombre,nivel,tipo_ciclo,numero_ciclos,datos,estado_actual_id,activo,tipo_origen,meta_origen,creado_por,actualizado_por,creado_en,actualizado_en,
carreras(id,facultad_id,nombre,nombre_corto,clave_sep,activa, facultades(id,nombre,nombre_corto,color,icono)) carreras(id,facultad_id,nombre,nombre_corto,clave_sep,activa, facultades(id,nombre,nombre_corto,color,icono))
@@ -49,7 +51,7 @@ export async function subjects_get(subjectId: UUID): Promise<Asignatura> {
export async function subjects_history( export async function subjects_history(
subjectId: UUID, subjectId: UUID,
): Promise<CambioAsignatura[]> { ): Promise<Array<CambioAsignatura>> {
const supabase = supabaseBrowser() const supabase = supabaseBrowser()
const { data, error } = await supabase const { data, error } = await supabase
.from('cambios_asignatura') .from('cambios_asignatura')
@@ -65,7 +67,7 @@ export async function subjects_history(
export async function subjects_bibliografia_list( export async function subjects_bibliografia_list(
subjectId: UUID, subjectId: UUID,
): Promise<BibliografiaAsignatura[]> { ): Promise<Array<BibliografiaAsignatura>> {
const supabase = supabaseBrowser() const supabase = supabaseBrowser()
const { data, error } = await supabase const { data, error } = await supabase
.from('bibliografia_asignatura') .from('bibliografia_asignatura')
@@ -112,9 +114,9 @@ export async function ai_generate_subject(payload: {
iaConfig: { iaConfig: {
descripcionEnfoque: string descripcionEnfoque: string
notasAdicionales?: string notasAdicionales?: string
archivosExistentesIds?: UUID[] archivosExistentesIds?: Array<UUID>
repositoriosIds?: UUID[] repositoriosIds?: Array<UUID>
archivosAdhocIds?: UUID[] archivosAdhocIds?: Array<UUID>
usarMCP?: boolean usarMCP?: boolean
} }
}): Promise<any> { }): Promise<any> {
@@ -145,7 +147,7 @@ export async function subjects_clone_from_existing(payload: {
export async function subjects_import_from_file(payload: { export async function subjects_import_from_file(payload: {
planId: UUID planId: UUID
archivoWordAsignaturaId: UUID archivoWordAsignaturaId: UUID
archivosAdicionalesIds?: UUID[] archivosAdicionalesIds?: Array<UUID>
}): Promise<Asignatura> { }): Promise<Asignatura> {
return invokeEdge<Asignatura>(EDGE.subjects_import_from_file, payload) return invokeEdge<Asignatura>(EDGE.subjects_import_from_file, payload)
} }
@@ -175,7 +177,7 @@ export async function subjects_update_fields(
export async function subjects_update_contenido( export async function subjects_update_contenido(
subjectId: UUID, subjectId: UUID,
unidades: any[], unidades: Array<any>,
): Promise<Asignatura> { ): Promise<Asignatura> {
return invokeEdge<Asignatura>(EDGE.subjects_update_contenido, { return invokeEdge<Asignatura>(EDGE.subjects_update_contenido, {
subjectId, subjectId,
@@ -224,3 +226,20 @@ export async function subjects_get_document(
subjectId, subjectId,
}) })
} }
export async function asignaturas_update(
asignaturaId: UUID,
patch: Partial<Asignatura>, // O tu tipo específico para el Patch de materias
): Promise<Asignatura> {
const supabase = supabaseBrowser()
const { data, error } = await supabase
.from('asignaturas')
.update(patch)
.eq('id', asignaturaId)
.select() // Trae la materia actualizada
.single()
throwIfError(error)
return requireData(data, 'No se pudo actualizar la asignatura.')
}

View File

@@ -1,13 +1,8 @@
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { qk } from '../query/keys'
import type { UUID } from '../types/domain'
import type {
BibliografiaUpsertInput,
SubjectsCreateManualInput,
SubjectsUpdateFieldsPatch,
} from '../api/subjects.api'
import { import {
ai_generate_subject, ai_generate_subject,
asignaturas_update,
subjects_bibliografia_list, subjects_bibliografia_list,
subjects_clone_from_existing, subjects_clone_from_existing,
subjects_create_manual, subjects_create_manual,
@@ -21,6 +16,14 @@ import {
subjects_update_contenido, subjects_update_contenido,
subjects_update_fields, subjects_update_fields,
} from '../api/subjects.api' } from '../api/subjects.api'
import { qk } from '../query/keys'
import type {
BibliografiaUpsertInput,
SubjectsCreateManualInput,
SubjectsUpdateFieldsPatch,
} from '../api/subjects.api'
import type { UUID } from '../types/domain'
export function useSubject(subjectId: UUID | null | undefined) { export function useSubject(subjectId: UUID | null | undefined) {
return useQuery({ return useQuery({
@@ -159,7 +162,7 @@ export function useUpdateSubjectContenido() {
const qc = useQueryClient() const qc = useQueryClient()
return useMutation({ return useMutation({
mutationFn: (vars: { subjectId: UUID; unidades: any[] }) => mutationFn: (vars: { subjectId: UUID; unidades: Array<any> }) =>
subjects_update_contenido(vars.subjectId, vars.unidades), subjects_update_contenido(vars.subjectId, vars.unidades),
onSuccess: (updated) => { onSuccess: (updated) => {
qc.setQueryData(qk.asignatura(updated.id), updated) qc.setQueryData(qk.asignatura(updated.id), updated)
@@ -194,3 +197,28 @@ export function useGenerateSubjectDocumento() {
}, },
}) })
} }
export function useUpdateAsignatura() {
const qc = useQueryClient()
return useMutation({
mutationFn: (vars: {
asignaturaId: UUID
patch: Partial<SubjectsUpdateFieldsPatch>
}) => asignaturas_update(vars.asignaturaId, vars.patch),
onSuccess: (updated) => {
// 1. Actualizamos la materia específica en la caché si tienes un query de "detalle"
qc.setQueryData(['asignatura', updated.id], updated)
// 2. IMPORTANTÍSIMO: Invalidamos la lista de materias del plan
// para que el mapa curricular vea los cambios (créditos, horas, nombre, etc.)
qc.invalidateQueries({
queryKey: ['plan_asignaturas', updated.plan_estudio_id],
})
// 3. Si tienes una lista general de asignaturas, también la invalidamos
qc.invalidateQueries({ queryKey: ['asignaturas', 'list'] })
},
})
}

View File

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

View File

@@ -1,4 +1,5 @@
import { useNavigate } from '@tanstack/react-router' import { useNavigate } from '@tanstack/react-router'
import * as Icons from 'lucide-react'
import { useNuevaAsignaturaWizard } from './hooks/useNuevaAsignaturaWizard' import { useNuevaAsignaturaWizard } from './hooks/useNuevaAsignaturaWizard'
@@ -6,11 +7,20 @@ import { PasoBasicosForm } from '@/components/asignaturas/wizard/PasoBasicosForm
import { PasoConfiguracionPanel } from '@/components/asignaturas/wizard/PasoConfiguracionPanel' import { PasoConfiguracionPanel } from '@/components/asignaturas/wizard/PasoConfiguracionPanel'
import { PasoMetodoCardGroup } from '@/components/asignaturas/wizard/PasoMetodoCardGroup' import { PasoMetodoCardGroup } from '@/components/asignaturas/wizard/PasoMetodoCardGroup'
import { PasoResumenCard } from '@/components/asignaturas/wizard/PasoResumenCard' import { PasoResumenCard } from '@/components/asignaturas/wizard/PasoResumenCard'
import { VistaSinPermisos } from '@/components/asignaturas/wizard/VistaSinPermisos'
import { WizardControls } from '@/components/asignaturas/wizard/WizardControls' import { WizardControls } from '@/components/asignaturas/wizard/WizardControls'
import { WizardHeader } from '@/components/asignaturas/wizard/WizardHeader'
import { defineStepper } from '@/components/stepper' 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( const Wizard = defineStepper(
{ {
@@ -24,8 +34,8 @@ const Wizard = defineStepper(
description: 'Nombre y estructura', description: 'Nombre y estructura',
}, },
{ {
id: 'configuracion', id: 'detalles',
title: 'Configuración', title: 'Detalles',
description: 'Detalles según modo', 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 }) { export function NuevaAsignaturaModalContainer({ planId }: { planId: string }) {
const navigate = useNavigate() const navigate = useNavigate()
const role = auth_get_current_user_role() const role = auth_get_current_user_role()
@@ -55,59 +63,46 @@ export function NuevaAsignaturaModalContainer({ planId }: { planId: string }) {
navigate({ to: `/planes/${planId}/asignaturas`, resetScroll: false }) 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 ( return (
<Dialog open={true} onOpenChange={(open) => !open && handleClose()}> <Wizard.Stepper.Provider
<DialogContent initialStep={Wizard.utils.getFirst().id}
className="flex h-[90vh] w-[calc(100%-2rem)] flex-col gap-0 overflow-hidden p-0 sm:max-w-4xl" className="flex h-full flex-col"
onInteractOutside={(e) => e.preventDefault()} >
> {({ methods }) => {
{role !== 'JEFE_CARRERA' ? ( const idx = Wizard.utils.getIndex(methods.current.id)
<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>
return (
<WizardLayout
title="Nueva Asignatura"
onClose={handleClose}
headerSlot={
<WizardResponsiveHeader wizard={Wizard} methods={methods} />
}
footerSlot={
<Wizard.Stepper.Controls>
<WizardControls <WizardControls
Wizard={Wizard} Wizard={Wizard}
methods={methods} methods={methods}
@@ -117,11 +112,41 @@ export function NuevaAsignaturaModalContainer({ planId }: { planId: string }) {
canContinueDesdeConfig={canContinueDesdeConfig} canContinueDesdeConfig={canContinueDesdeConfig}
onCreate={() => crearAsignatura(handleClose)} onCreate={() => crearAsignatura(handleClose)}
/> />
</> </Wizard.Stepper.Controls>
)} }
</Wizard.Stepper.Provider> >
)} <div className="mx-auto max-w-3xl">
</DialogContent> {idx === 0 && (
</Dialog> <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 { PasoModoCardGroup } from '@/components/planes/wizard/PasoModoCardGroup'
import { PasoResumenCard } from '@/components/planes/wizard/PasoResumenCard' import { PasoResumenCard } from '@/components/planes/wizard/PasoResumenCard'
import { WizardControls } from '@/components/planes/wizard/WizardControls' import { WizardControls } from '@/components/planes/wizard/WizardControls'
import { WizardHeader } from '@/components/planes/wizard/WizardHeader'
import { defineStepper } from '@/components/stepper' import { defineStepper } from '@/components/stepper'
import { import {
Card, Card,
@@ -19,16 +18,12 @@ import {
CardHeader, CardHeader,
CardTitle, CardTitle,
} from '@/components/ui/card' } from '@/components/ui/card'
import { import { WizardLayout } from '@/components/wizard/WizardLayout'
Dialog, import { WizardResponsiveHeader } from '@/components/wizard/WizardResponsiveHeader'
DialogContent,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
// import { useGeneratePlanAI } from '@/data/hooks/usePlans' // import { useGeneratePlanAI } from '@/data/hooks/usePlans'
// Mock de permisos/rol // 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( const Wizard = defineStepper(
{ {
@@ -64,136 +59,97 @@ export default function NuevoPlanModalContainer() {
// Crear plan: ahora la lógica vive en WizardControls // 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 ( return (
<Dialog open={true} onOpenChange={(open) => !open && handleClose()}> <Wizard.Stepper.Provider
<DialogContent initialStep={Wizard.utils.getFirst().id}
className="flex h-[90vh] w-[calc(100%-2rem)] flex-col gap-0 overflow-hidden p-0 sm:max-w-4xl" className="flex h-full flex-col"
onInteractOutside={(e) => { >
e.preventDefault() {({ methods }) => {
}} const idx = Wizard.utils.getIndex(methods.current.id)
>
{role !== 'JEFE_CARRERA' ? ( return (
<> <WizardLayout
<DialogHeader className="flex-none border-b p-6"> title="Nuevo plan de estudios"
<DialogTitle>Nuevo plan de estudios</DialogTitle> onClose={handleClose}
</DialogHeader> headerSlot={
<div className="flex-1 p-6"> <WizardResponsiveHeader wizard={Wizard} methods={methods} />
<Card className="border-destructive/40"> }
<CardHeader> footerSlot={
<CardTitle className="flex items-center gap-2"> <Wizard.Stepper.Controls>
<Icons.ShieldAlert className="text-destructive h-5 w-5" /> <WizardControls
Sin permisos errorMessage={wizard.errorMessage}
</CardTitle> onPrev={() => methods.prev()}
<CardDescription> onNext={() => methods.next()}
No tienes permisos para crear planes de estudio. disablePrev={idx === 0 || wizard.isLoading}
</CardDescription> disableNext={
</CardHeader> wizard.isLoading ||
<CardContent className="flex justify-end"> (idx === 0 && !canContinueDesdeModo) ||
<button (idx === 1 && !canContinueDesdeBasicos) ||
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" (idx === 2 && !canContinueDesdeDetalles)
onClick={handleClose} }
> disableCreate={wizard.isLoading}
Volver isLastStep={idx >= Wizard.steps.length - 1}
</button> wizard={wizard}
</CardContent> setWizard={setWizard}
</Card> />
</div> </Wizard.Stepper.Controls>
</> }
) : (
<Wizard.Stepper.Provider
initialStep={Wizard.utils.getFirst().id}
className="flex h-full flex-col"
> >
{({ methods }) => { <div className="mx-auto max-w-3xl">
const currentIndex = Wizard.utils.getIndex(methods.current.id) + 1 {idx === 0 && (
const totalSteps = Wizard.steps.length <Wizard.Stepper.Panel>
const nextStep = Wizard.steps[currentIndex] ?? { <PasoModoCardGroup wizard={wizard} onChange={setWizard} />
title: '', </Wizard.Stepper.Panel>
description: '', )}
} {idx === 1 && (
<Wizard.Stepper.Panel>
return ( <PasoBasicosForm wizard={wizard} onChange={setWizard} />
<> </Wizard.Stepper.Panel>
<WizardHeader )}
currentIndex={currentIndex} {idx === 2 && (
totalSteps={totalSteps} <Wizard.Stepper.Panel>
currentTitle={methods.current.title} <PasoDetallesPanel
currentDescription={methods.current.description} wizard={wizard}
nextTitle={nextStep.title} onChange={setWizard}
onClose={handleClose} isLoading={wizard.isLoading}
Wizard={Wizard}
/> />
</Wizard.Stepper.Panel>
<div className="flex-1 overflow-y-auto bg-gray-50/30 p-6"> )}
<div className="mx-auto max-w-3xl"> {idx === 3 && (
{Wizard.utils.getIndex(methods.current.id) === 0 && ( <Wizard.Stepper.Panel>
<Wizard.Stepper.Panel> <PasoResumenCard wizard={wizard} />
<PasoModoCardGroup </Wizard.Stepper.Panel>
wizard={wizard} )}
onChange={setWizard} </div>
/> </WizardLayout>
</Wizard.Stepper.Panel> )
)} }}
{Wizard.utils.getIndex(methods.current.id) === 1 && ( </Wizard.Stepper.Provider>
<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>
) )
} }

View File

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

View File

@@ -35,6 +35,8 @@ export const Route = createFileRoute('/planes/$planId/_detalle')({
} catch (e: any) { } catch (e: any) {
// PGRST116: The result contains 0 rows // PGRST116: The result contains 0 rows
if (e?.code === 'PGRST116') { if (e?.code === 'PGRST116') {
console.log('not found on', Route.path)
throw notFound() throw notFound()
} }
throw e throw e
@@ -219,7 +221,11 @@ function RouteComponent() {
<Tab to="/planes/$planId/" params={{ planId }}> <Tab to="/planes/$planId/" params={{ planId }}>
Datos Generales Datos Generales
</Tab> </Tab>
<Tab to="/planes/$planId/mapa" params={{ planId }}> <Tab
to="/planes/$planId/mapa"
params={{ planId }}
search={{ ciclo: data?.numero_ciclos }}
>
Mapa Curricular Mapa Curricular
</Tab> </Tab>
<Tab to="/planes/$planId/asignaturas" params={{ planId }}> <Tab to="/planes/$planId/asignaturas" params={{ planId }}>
@@ -234,7 +240,13 @@ function RouteComponent() {
<Tab to="/planes/$planId/documento" params={{ planId }}> <Tab to="/planes/$planId/documento" params={{ planId }}>
Documento Documento
</Tab> </Tab>
<Tab to="/planes/$planId/historial" params={{ planId }}> <Tab
to="/planes/$planId/historial"
params={{ planId }}
search={{
structure: data?.estructuras_plan?.definicion?.properties,
}}
>
Historial Historial
</Tab> </Tab>
</nav> </nav>
@@ -288,16 +300,20 @@ const InfoCard = forwardRef<
function Tab({ function Tab({
to, to,
params, params,
search,
children, children,
}: { }: {
to: string to: string
params?: any params?: any
search?: any
children: React.ReactNode children: React.ReactNode
}) { }) {
console.log(search)
return ( return (
<Link <Link
to={to} to={to}
params={params} params={params}
search={search}
className="border-b-2 border-transparent pb-3 text-sm font-medium text-slate-500 transition-all hover:text-slate-800" className="border-b-2 border-transparent pb-3 text-sm font-medium text-slate-500 transition-all hover:text-slate-800"
activeProps={{ className: 'border-teal-600 text-teal-700 font-bold' }} activeProps={{ className: 'border-teal-600 text-teal-700 font-bold' }}
activeOptions={{ activeOptions={{

View File

@@ -10,7 +10,7 @@ import {
} from 'lucide-react' } from 'lucide-react'
import { useState, useMemo } from '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 { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
@@ -33,17 +33,24 @@ import {
import { usePlanAsignaturas, usePlanLineas } from '@/data' import { usePlanAsignaturas, usePlanLineas } from '@/data'
// --- Configuración de Estilos --- // --- 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' }, borrador: { label: 'Borrador', className: 'bg-slate-100 text-slate-600' },
revisada: { label: 'Revisada', className: 'bg-amber-100 text-amber-700' }, revisada: { label: 'Revisada', className: 'bg-amber-100 text-amber-700' },
aprobada: { label: 'Aprobada', className: 'bg-emerald-100 text-emerald-700' }, aprobada: { label: 'Aprobada', className: 'bg-emerald-100 text-emerald-700' },
} }
const tipoConfig: Record<string, { label: string; className: string }> = { 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' }, obligatoria: {
troncal: { label: 'Troncal', className: 'bg-slate-100 text-slate-700' }, 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 --- // --- Mapeadores de API ---
const mapAsignaturas = (asigApi: Array<any> = []): Array<Asignatura> => { 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 estado: 'borrador', // O el campo que venga de tu API
hd: Math.floor((asig.horas_semana ?? 0) / 2), hd: Math.floor((asig.horas_semana ?? 0) / 2),
hi: Math.ceil((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, component: AsignaturasPage,
}) })
@@ -131,7 +141,17 @@ function AsignaturasPage() {
<Button variant="outline" size="sm"> <Button variant="outline" size="sm">
<Copy className="mr-2 h-4 w-4" /> Clonar <Copy className="mr-2 h-4 w-4" /> Clonar
</Button> </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 <Plus className="mr-2 h-4 w-4" /> Nueva Asignatura
</Button> </Button>
</div> </div>
@@ -262,17 +282,17 @@ function AsignaturasPage() {
<TableCell> <TableCell>
<Badge <Badge
variant="outline" 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> </Badge>
</TableCell> </TableCell>
<TableCell> <TableCell>
<Badge <Badge
variant="outline" 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> </Badge>
</TableCell> </TableCell>
<TableCell> <TableCell>

View File

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

View File

@@ -27,6 +27,9 @@ import { usePlanHistorial } from '@/data/hooks/usePlans'
export const Route = createFileRoute('/planes/$planId/_detalle/historial')({ export const Route = createFileRoute('/planes/$planId/_detalle/historial')({
component: RouteComponent, component: RouteComponent,
validateSearch: (search: { structure?: any }) => ({
structure: search.structure ?? null,
}),
}) })
const getEventConfig = (tipo: string, campo: string) => { const getEventConfig = (tipo: string, campo: string) => {
@@ -58,6 +61,9 @@ const getEventConfig = (tipo: string, campo: string) => {
function RouteComponent() { function RouteComponent() {
const { planId } = Route.useParams() const { planId } = Route.useParams()
const { data: rawData, isLoading } = usePlanHistorial(planId) const { data: rawData, isLoading } = usePlanHistorial(planId)
const { structure } = Route.useSearch()
console.log(structure?.vigencia?.title)
console.log(structure)
// ESTADOS PARA EL MODAL // ESTADOS PARA EL MODAL
const [selectedEvent, setSelectedEvent] = useState<any>(null) const [selectedEvent, setSelectedEvent] = useState<any>(null)
@@ -77,7 +83,9 @@ function RouteComponent() {
description: description:
item.campo === 'datos' item.campo === 'datos'
? `Actualización general de: ${item.valor_nuevo?.nombre || 'información del plan'}` ? `Actualización general de: ${item.valor_nuevo?.nombre || 'información del plan'}`
: `Se modificó el campo ${item.campo}`, : `Se modificó el campo ${
structure?.[item.campo]?.title ?? item.campo
}`,
date: parseISO(item.cambiado_en), date: parseISO(item.cambiado_en),
icon: config.icon, icon: config.icon,
campo: item.campo, campo: item.campo,

View File

@@ -18,7 +18,7 @@ import {
TooltipProvider, TooltipProvider,
TooltipTrigger, TooltipTrigger,
} from '@/components/ui/tooltip' } from '@/components/ui/tooltip'
import { usePlan } from '@/data' import { usePlan, useUpdatePlanFields } from '@/data'
// import { toast } from 'sonner' // Asegúrate de tener sonner instalado o quita la línea // import { toast } from 'sonner' // Asegúrate de tener sonner instalado o quita la línea
export const Route = createFileRoute('/planes/$planId/_detalle/')({ export const Route = createFileRoute('/planes/$planId/_detalle/')({
@@ -39,7 +39,7 @@ function DatosGeneralesPage() {
const [editingId, setEditingId] = useState<string | null>(null) const [editingId, setEditingId] = useState<string | null>(null)
const [editValue, setEditValue] = useState('') const [editValue, setEditValue] = useState('')
const location = useLocation() const location = useLocation()
const updatePlan = useUpdatePlanFields()
// Confetti al llegar desde creación // Confetti al llegar desde creación
useEffect(() => { useEffect(() => {
if (location.state.showConfetti) { if (location.state.showConfetti) {
@@ -122,14 +122,47 @@ function DatosGeneralesPage() {
setEditValue('') setEditValue('')
} }
const handleSave = (id: string) => { const handleSave = (campo: DatosGeneralesField) => {
// Actualizamos el estado local de la lista if (!data?.datos) return
const currentValue = (data.datos as any)[campo.clave]
let newValue: any
if (
typeof currentValue === 'object' &&
currentValue !== null &&
'description' in currentValue
) {
// Caso 1: objeto con description
newValue = {
...currentValue,
description: editValue,
}
} else {
// Caso 2: valor plano (string, number, etc)
newValue = editValue
}
const datosActualizados = {
...data.datos,
[campo.clave]: newValue,
}
updatePlan.mutate({
planId,
patch: {
datos: datosActualizados,
},
})
// UI optimista
setCampos((prev) => setCampos((prev) =>
prev.map((c) => (c.id === id ? { ...c, value: editValue } : c)), prev.map((c) => (c.id === campo.id ? { ...c, value: editValue } : c)),
) )
setEditingId(null) setEditingId(null)
setEditValue('') setEditValue('')
// toast.success('Cambios guardados localmente')
} }
const handleIARequest = (clave: string) => { const handleIARequest = (clave: string) => {
@@ -245,7 +278,7 @@ function DatosGeneralesPage() {
<Button <Button
size="sm" size="sm"
className="bg-teal-600 hover:bg-teal-700" className="bg-teal-600 hover:bg-teal-700"
onClick={() => handleSave(campo.id)} onClick={() => handleSave(campo)}
> >
<Check size={14} className="mr-1" /> Guardar <Check size={14} className="mr-1" /> Guardar
</Button> </Button>

View File

@@ -33,7 +33,7 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from '@/components/ui/select' } from '@/components/ui/select'
import { usePlanAsignaturas, usePlanLineas } from '@/data' import { usePlanAsignaturas, usePlanLineas, useUpdateAsignatura } from '@/data'
// --- Mapeadores (Fuera del componente para mayor limpieza) --- // --- Mapeadores (Fuera del componente para mayor limpieza) ---
const mapLineasToLineaCurricular = ( const mapLineasToLineaCurricular = (
@@ -60,8 +60,9 @@ const mapAsignaturasToAsignaturas = (
tipo: asig.tipo === 'OBLIGATORIA' ? 'obligatoria' : 'optativa', tipo: asig.tipo === 'OBLIGATORIA' ? 'obligatoria' : 'optativa',
estado: 'borrador', estado: 'borrador',
orden: asig.orden_celda ?? 0, orden: asig.orden_celda ?? 0,
hd: Math.floor((asig.horas_semana ?? 0) / 2), // Mapeo directo de los nuevos campos de la API
hi: Math.ceil((asig.horas_semana ?? 0) / 2), hd: asig.horas_academicas ?? 0,
hi: asig.horas_independientes ?? 0,
prerrequisitos: [], prerrequisitos: [],
})) }))
} }
@@ -157,10 +158,15 @@ function AsignaturaCardItem({
export const Route = createFileRoute('/planes/$planId/_detalle/mapa')({ export const Route = createFileRoute('/planes/$planId/_detalle/mapa')({
component: MapaCurricularPage, component: MapaCurricularPage,
validateSearch: (search: { ciclo?: number }) => ({
ciclo: search.ciclo ?? null,
}),
}) })
function MapaCurricularPage() { function MapaCurricularPage() {
const { planId } = Route.useParams() // Idealmente usa el ID de la ruta const { planId } = Route.useParams() // Idealmente usa el ID de la ruta
const { ciclo } = Route.useSearch()
console.log(ciclo)
// 1. Fetch de Datos // 1. Fetch de Datos
const { data: asignaturasApi, isLoading: loadingAsig } = const { data: asignaturasApi, isLoading: loadingAsig } =
@@ -178,6 +184,7 @@ function MapaCurricularPage() {
useState<Asignatura | null>(null) useState<Asignatura | null>(null)
const [hasAreaComun, setHasAreaComun] = useState(false) const [hasAreaComun, setHasAreaComun] = useState(false)
const [nombreNuevaLinea, setNombreNuevaLinea] = useState('') // Para el input de nombre personalizado const [nombreNuevaLinea, setNombreNuevaLinea] = useState('') // Para el input de nombre personalizado
const { mutate: updateAsignatura, isPending } = useUpdateAsignatura()
const manejarAgregarLinea = (nombre: string) => { const manejarAgregarLinea = (nombre: string) => {
const nombreNormalizado = nombre.trim() const nombreNormalizado = nombre.trim()
@@ -249,7 +256,7 @@ function MapaCurricularPage() {
if (lineasApi) setLineas(mapLineasToLineaCurricular(lineasApi)) if (lineasApi) setLineas(mapLineasToLineaCurricular(lineasApi))
}, [lineasApi]) }, [lineasApi])
const ciclosTotales = 9 const ciclosTotales = Number(ciclo)
const ciclosArray = Array.from({ length: ciclosTotales }, (_, i) => i + 1) const ciclosArray = Array.from({ length: ciclosTotales }, (_, i) => i + 1)
// Nuevo estado para controlar los datos temporales del modal de edición // Nuevo estado para controlar los datos temporales del modal de edición
@@ -263,13 +270,41 @@ function MapaCurricularPage() {
setAsignaturas((prev) => setAsignaturas((prev) =>
prev.map((m) => (m.id === editingData.id ? { ...editingData } : m)), prev.map((m) => (m.id === editingData.id ? { ...editingData } : m)),
) )
setIsEditModalOpen(false) // setIsEditModalOpen(false)
// Preparamos el patch con la estructura de tu tabla
const patch = {
nombre: editingData.nombre,
codigo: editingData.clave,
creditos: editingData.creditos,
horas_academicas: editingData.hd,
horas_independientes: editingData.hi,
numero_ciclo: editingData.ciclo,
linea_plan_id: editingData.lineaCurricularId,
tipo: editingData.tipo.toUpperCase(), // Asegurar que coincida con el ENUM (OBLIGATORIA/OPTATIVA)
// datos: editingData.datos, // Si editaste algo del JSONB
}
updateAsignatura(
{ asignaturaId: editingData.id, patch },
{
onSuccess: () => {
setIsEditModalOpen(false)
// Opcional: Mostrar un toast de éxito
},
onError: (error) => {
console.error('Error al guardar:', error)
alert('Hubo un error al guardar los cambios.')
},
},
)
} }
// 2. MODIFICACIÓN: Zona de soltado siempre visible // 2. MODIFICACIÓN: Zona de soltado siempre visible
// Cambiamos la condición: Mostramos la sección si hay asignaturas sin asignar // Cambiamos la condición: Mostramos la sección si hay asignaturas sin asignar
// O si simplemente queremos tener el "depósito" disponible. // O si simplemente queremos tener el "depósito" disponible.
const unassignedAsignaturas = asignaturas.filter((m) => m.ciclo === null) const unassignedAsignaturas = asignaturas.filter(
(m) => m.ciclo === null || m.lineaCurricularId === null,
)
// --- Lógica de Gestión --- // --- Lógica de Gestión ---
const agregarLinea = (nombre: string) => { const agregarLinea = (nombre: string) => {
@@ -304,7 +339,7 @@ function MapaCurricularPage() {
const getSubtotalLinea = (lineaId: string) => { const getSubtotalLinea = (lineaId: string) => {
return asignaturas return asignaturas
.filter((m) => m.lineaCurricularId === lineaId && m.ciclo !== null) .filter((m) => m.lineaCurricularId === lineaId && m.ciclo !== null) // Aseguramos que pertenezca a la línea Y tenga ciclo
.reduce( .reduce(
(acc, m) => ({ (acc, m) => ({
cr: acc.cr + (m.creditos || 0), cr: acc.cr + (m.creditos || 0),
@@ -327,6 +362,7 @@ function MapaCurricularPage() {
) => { ) => {
e.preventDefault() e.preventDefault()
if (draggedAsignatura) { if (draggedAsignatura) {
// 1. Actualización optimista del UI
setAsignaturas((prev) => setAsignaturas((prev) =>
prev.map((m) => prev.map((m) =>
m.id === draggedAsignatura m.id === draggedAsignatura
@@ -334,6 +370,23 @@ function MapaCurricularPage() {
: m, : m,
), ),
) )
// 2. Persistir en la API
const patch = {
numero_ciclo: ciclo,
linea_plan_id: lineaId,
}
updateAsignatura(
{ asignaturaId: draggedAsignatura, patch },
{
onError: (error) => {
console.error('Error al mover:', error)
// Opcional: Revertir el estado local si falla
},
},
)
setDraggedAsignatura(null) setDraggedAsignatura(null)
} }
} }
@@ -368,10 +421,15 @@ function MapaCurricularPage() {
</p> </p>
</div> </div>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
{asignaturas.filter((m) => !m.ciclo).length > 0 && ( {asignaturas.filter((m) => !m.ciclo || !m.lineaCurricularId).length >
0 && (
<Badge className="border-amber-100 bg-amber-50 text-amber-600 hover:bg-amber-50"> <Badge className="border-amber-100 bg-amber-50 text-amber-600 hover:bg-amber-50">
<AlertTriangle size={14} className="mr-1" />{' '} <AlertTriangle size={14} className="mr-1" />{' '}
{asignaturas.filter((m) => !m.ciclo).length} sin asignar {
asignaturas.filter((m) => !m.ciclo || !m.lineaCurricularId)
.length
}{' '}
sin asignar
</Badge> </Badge>
)} )}
<DropdownMenu> <DropdownMenu>
@@ -477,6 +535,8 @@ function MapaCurricularPage() {
<div <div
key={ciclo} key={ciclo}
onDragOver={handleDragOver} onDragOver={handleDragOver}
// ANTES: onDrop={(e) => handleDrop(e, null, null)}
// AHORA: Usamos las variables 'ciclo' y 'linea.id' del map
onDrop={(e) => handleDrop(e, ciclo, linea.id)} onDrop={(e) => handleDrop(e, ciclo, linea.id)}
className="min-h-[140px] space-y-2 rounded-xl border-2 border-dashed border-slate-100 bg-slate-50/20 p-2" className="min-h-[140px] space-y-2 rounded-xl border-2 border-dashed border-slate-100 bg-slate-50/20 p-2"
> >
@@ -588,7 +648,10 @@ function MapaCurricularPage() {
{/* Modal de Edición */} {/* Modal de Edición */}
<Dialog open={isEditModalOpen} onOpenChange={setIsEditModalOpen}> <Dialog open={isEditModalOpen} onOpenChange={setIsEditModalOpen}>
<DialogContent className="sm:max-w-[550px]"> <DialogContent
className="sm:max-w-[550px]"
onInteractOutside={(e) => e.preventDefault()}
>
<DialogHeader> <DialogHeader>
<DialogTitle className="font-bold text-slate-700"> <DialogTitle className="font-bold text-slate-700">
Editar Asignatura Editar Asignatura
@@ -734,32 +797,61 @@ function MapaCurricularPage() {
</div> </div>
</div> </div>
{/* Fila 4: Seriación (Igual a tu imagen) */} {/* Fila 4: Seriación (Prerrequisitos) */}
<div className="space-y-2"> <div className="space-y-2">
<label className="text-xs font-bold text-slate-500 uppercase"> <label className="text-xs font-bold text-slate-500 uppercase">
Seriación (Prerrequisitos) Seriación (Prerrequisitos)
</label> </label>
<Select> <Select
onValueChange={(val) => {
// Evitamos duplicados en la lista de prerrequisitos local
if (!editingData.prerrequisitos.includes(val)) {
setEditingData({
...editingData,
prerrequisitos: [...editingData.prerrequisitos, val],
})
}
}}
>
<SelectTrigger> <SelectTrigger>
<SelectValue placeholder="Seleccionar asignatura..." /> <SelectValue placeholder="Seleccionar asignatura..." />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{asignaturas.map((m) => ( {/* FILTRO CLAVE:
<SelectItem key={m.id} value={m.clave}> Solo mostramos materias cuyo ID sea diferente al de la materia que estamos editando
{m.nombre} */}
</SelectItem> {asignaturas
))} .filter((m) => m.id !== editingData.id)
.map((m) => (
<SelectItem key={m.id} value={m.clave}>
{m.nombre} ({m.clave})
</SelectItem>
))}
</SelectContent> </SelectContent>
</Select> </Select>
<div className="mt-2 flex gap-2">
{/* Aquí usamos el array vacío que inicializamos en el mapeador */} {/* Visualización de los prerrequisitos seleccionados */}
<div className="mt-2 flex flex-wrap gap-2">
{editingData.prerrequisitos.map((pre) => ( {editingData.prerrequisitos.map((pre) => (
<Badge <Badge
key={pre} key={pre}
variant="secondary" variant="secondary"
className="bg-slate-100 text-slate-600" className="bg-slate-100 text-slate-600"
> >
{pre} <span className="ml-1 cursor-pointer">×</span> {pre}
<button
className="ml-1 hover:text-red-500"
onClick={() => {
setEditingData({
...editingData,
prerrequisitos: editingData.prerrequisitos.filter(
(p) => p !== pre,
),
})
}}
>
×
</button>
</Badge> </Badge>
))} ))}
</div> </div>

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>
</div> </div>
<button <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" 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 <Icons.Plus /> Nuevo plan de estudios

File diff suppressed because it is too large Load Diff