6 Commits

24 changed files with 1888 additions and 1607 deletions

View File

@@ -442,20 +442,13 @@ function DatosGenerales({
? 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]
// 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 =
!valActual?.description ||
valActual.description === config.description
const currentContent = valActual?.description ?? ''
const currentContent = valActual ?? ''
return (
<InfoCard

View File

@@ -23,7 +23,6 @@ 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 && (
@@ -61,6 +60,5 @@ export function WizardControls({
)}
</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
.from('asignaturas')
.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)
.order('numero_ciclo', { ascending: true, nullsFirst: false })

View File

@@ -1,6 +1,9 @@
import { supabaseBrowser } from '../supabase/client'
import { invokeEdge } from '../supabase/invokeEdge'
import { throwIfError, requireData } from './_helpers'
import type { DocumentoResult } from './plans.api'
import type {
Asignatura,
BibliografiaAsignatura,
@@ -8,7 +11,6 @@ import type {
TipoAsignatura,
UUID,
} from '../types/domain'
import type { DocumentoResult } from './plans.api'
const EDGE = {
subjects_create_manual: 'subjects_create_manual',
@@ -32,7 +34,7 @@ export async function subjects_get(subjectId: UUID): Promise<Asignatura> {
.from('asignaturas')
.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(
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))
@@ -49,7 +51,7 @@ export async function subjects_get(subjectId: UUID): Promise<Asignatura> {
export async function subjects_history(
subjectId: UUID,
): Promise<CambioAsignatura[]> {
): Promise<Array<CambioAsignatura>> {
const supabase = supabaseBrowser()
const { data, error } = await supabase
.from('cambios_asignatura')
@@ -65,7 +67,7 @@ export async function subjects_history(
export async function subjects_bibliografia_list(
subjectId: UUID,
): Promise<BibliografiaAsignatura[]> {
): Promise<Array<BibliografiaAsignatura>> {
const supabase = supabaseBrowser()
const { data, error } = await supabase
.from('bibliografia_asignatura')
@@ -112,9 +114,9 @@ export async function ai_generate_subject(payload: {
iaConfig: {
descripcionEnfoque: string
notasAdicionales?: string
archivosExistentesIds?: UUID[]
repositoriosIds?: UUID[]
archivosAdhocIds?: UUID[]
archivosExistentesIds?: Array<UUID>
repositoriosIds?: Array<UUID>
archivosAdhocIds?: Array<UUID>
usarMCP?: boolean
}
}): Promise<any> {
@@ -145,7 +147,7 @@ export async function subjects_clone_from_existing(payload: {
export async function subjects_import_from_file(payload: {
planId: UUID
archivoWordAsignaturaId: UUID
archivosAdicionalesIds?: UUID[]
archivosAdicionalesIds?: Array<UUID>
}): Promise<Asignatura> {
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(
subjectId: UUID,
unidades: any[],
unidades: Array<any>,
): Promise<Asignatura> {
return invokeEdge<Asignatura>(EDGE.subjects_update_contenido, {
subjectId,
@@ -224,3 +226,20 @@ export async function subjects_get_document(
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 { qk } from '../query/keys'
import type { UUID } from '../types/domain'
import type {
BibliografiaUpsertInput,
SubjectsCreateManualInput,
SubjectsUpdateFieldsPatch,
} from '../api/subjects.api'
import {
ai_generate_subject,
asignaturas_update,
subjects_bibliografia_list,
subjects_clone_from_existing,
subjects_create_manual,
@@ -21,6 +16,14 @@ import {
subjects_update_contenido,
subjects_update_fields,
} 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) {
return useQuery({
@@ -159,7 +162,7 @@ export function useUpdateSubjectContenido() {
const qc = useQueryClient()
return useMutation({
mutationFn: (vars: { subjectId: UUID; unidades: any[] }) =>
mutationFn: (vars: { subjectId: UUID; unidades: Array<any> }) =>
subjects_update_contenido(vars.subjectId, vars.unidades),
onSuccess: (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 = {
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
{({ methods }) => {
const idx = Wizard.utils.getIndex(methods.current.id)
return (
<WizardLayout
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>
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.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>
)}
</DialogContent>
</Dialog>
)
}

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,20 +59,9 @@ export default function NuevoPlanModalContainer() {
// Crear plan: ahora la lógica vive en WizardControls
if (role !== 'JEFE_CARRERA') {
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">
<WizardLayout title="Nuevo plan de estudios" onClose={handleClose}>
<Card className="border-destructive/40">
<CardHeader>
<CardTitle className="flex items-center gap-2">
@@ -97,52 +81,58 @@ export default function NuevoPlanModalContainer() {
</button>
</CardContent>
</Card>
</div>
</>
) : (
</WizardLayout>
)
}
return (
<Wizard.Stepper.Provider
initialStep={Wizard.utils.getFirst().id}
className="flex h-full flex-col"
>
{({ methods }) => {
const currentIndex = Wizard.utils.getIndex(methods.current.id) + 1
const totalSteps = Wizard.steps.length
const nextStep = Wizard.steps[currentIndex] ?? {
title: '',
description: '',
}
const idx = Wizard.utils.getIndex(methods.current.id)
return (
<>
<WizardHeader
currentIndex={currentIndex}
totalSteps={totalSteps}
currentTitle={methods.current.title}
currentDescription={methods.current.description}
nextTitle={nextStep.title}
<WizardLayout
title="Nuevo plan de estudios"
onClose={handleClose}
Wizard={Wizard}
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}
/>
<div className="flex-1 overflow-y-auto bg-gray-50/30 p-6">
</Wizard.Stepper.Controls>
}
>
<div className="mx-auto max-w-3xl">
{Wizard.utils.getIndex(methods.current.id) === 0 && (
{idx === 0 && (
<Wizard.Stepper.Panel>
<PasoModoCardGroup
wizard={wizard}
onChange={setWizard}
/>
<PasoModoCardGroup wizard={wizard} onChange={setWizard} />
</Wizard.Stepper.Panel>
)}
{Wizard.utils.getIndex(methods.current.id) === 1 && (
{idx === 1 && (
<Wizard.Stepper.Panel>
<PasoBasicosForm
wizard={wizard}
onChange={setWizard}
/>
<PasoBasicosForm wizard={wizard} onChange={setWizard} />
</Wizard.Stepper.Panel>
)}
{Wizard.utils.getIndex(methods.current.id) === 2 && (
{idx === 2 && (
<Wizard.Stepper.Panel>
<PasoDetallesPanel
wizard={wizard}
@@ -151,49 +141,15 @@ export default function NuevoPlanModalContainer() {
/>
</Wizard.Stepper.Panel>
)}
{Wizard.utils.getIndex(methods.current.id) === 3 && (
{idx === 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>
</>
</WizardLayout>
)
}}
</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 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,29 +41,22 @@ 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 PlanesPlanIdAsignaturasRouteRoute =
PlanesPlanIdAsignaturasRouteRouteImport.update({
id: '/planes/$planId/asignaturas',
path: '/planes/$planId/asignaturas',
getParentRoute: () => rootRouteImport,
} as any)
const PlanesPlanIdDetalleRouteRoute =
PlanesPlanIdDetalleRouteRouteImport.update({
const PlanesPlanIdDetalleRoute = PlanesPlanIdDetalleRouteImport.update({
id: '/planes/$planId/_detalle',
path: '/planes/$planId',
getParentRoute: () => rootRouteImport,
@@ -74,116 +65,109 @@ 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,
const PlanesPlanIdDetalleRouteChildren: PlanesPlanIdDetalleRouteChildren = {
PlanesPlanIdDetalleDocumentoRoute: PlanesPlanIdDetalleDocumentoRoute,
PlanesPlanIdDetalleFlujoRoute: PlanesPlanIdDetalleFlujoRoute,
PlanesPlanIdDetalleHistorialRoute: PlanesPlanIdDetalleHistorialRoute,
PlanesPlanIdDetalleIaplanRoute: PlanesPlanIdDetalleIaplanRoute,
PlanesPlanIdDetalleMapaRoute: PlanesPlanIdDetalleMapaRoute,
PlanesPlanIdDetalleIndexRoute: PlanesPlanIdDetalleIndexRoute,
PlanesPlanIdDetalleAsignaturasNuevaRoute:
PlanesPlanIdDetalleAsignaturasNuevaRoute,
PlanesPlanIdDetalleAsignaturasIndexRoute:
PlanesPlanIdDetalleAsignaturasIndexRoute,
}
const PlanesPlanIdDetalleRouteRouteWithChildren =
PlanesPlanIdDetalleRouteRoute._addFileChildren(
PlanesPlanIdDetalleRouteRouteChildren,
)
interface PlanesPlanIdAsignaturasListaRouteRouteChildren {
PlanesPlanIdAsignaturasListaNuevaRoute: typeof PlanesPlanIdAsignaturasListaNuevaRoute
}
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
@@ -219,7 +221,11 @@ function RouteComponent() {
<Tab to="/planes/$planId/" params={{ planId }}>
Datos Generales
</Tab>
<Tab to="/planes/$planId/mapa" params={{ planId }}>
<Tab
to="/planes/$planId/mapa"
params={{ planId }}
search={{ ciclo: data?.numero_ciclos }}
>
Mapa Curricular
</Tab>
<Tab to="/planes/$planId/asignaturas" params={{ planId }}>
@@ -234,7 +240,13 @@ function RouteComponent() {
<Tab to="/planes/$planId/documento" params={{ planId }}>
Documento
</Tab>
<Tab to="/planes/$planId/historial" params={{ planId }}>
<Tab
to="/planes/$planId/historial"
params={{ planId }}
search={{
structure: data?.estructuras_plan?.definicion?.properties,
}}
>
Historial
</Tab>
</nav>
@@ -288,16 +300,20 @@ const InfoCard = forwardRef<
function Tab({
to,
params,
search,
children,
}: {
to: string
params?: any
search?: any
children: React.ReactNode
}) {
console.log(search)
return (
<Link
to={to}
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"
activeProps={{ className: 'border-teal-600 text-teal-700 font-bold' }}
activeOptions={{

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,14 +33,21 @@ 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' },
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' },
}
@@ -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

@@ -27,6 +27,9 @@ import { usePlanHistorial } from '@/data/hooks/usePlans'
export const Route = createFileRoute('/planes/$planId/_detalle/historial')({
component: RouteComponent,
validateSearch: (search: { structure?: any }) => ({
structure: search.structure ?? null,
}),
})
const getEventConfig = (tipo: string, campo: string) => {
@@ -58,6 +61,9 @@ const getEventConfig = (tipo: string, campo: string) => {
function RouteComponent() {
const { planId } = Route.useParams()
const { data: rawData, isLoading } = usePlanHistorial(planId)
const { structure } = Route.useSearch()
console.log(structure?.vigencia?.title)
console.log(structure)
// ESTADOS PARA EL MODAL
const [selectedEvent, setSelectedEvent] = useState<any>(null)
@@ -77,7 +83,9 @@ function RouteComponent() {
description:
item.campo === 'datos'
? `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),
icon: config.icon,
campo: item.campo,

View File

@@ -18,7 +18,7 @@ import {
TooltipProvider,
TooltipTrigger,
} 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
export const Route = createFileRoute('/planes/$planId/_detalle/')({
@@ -39,7 +39,7 @@ function DatosGeneralesPage() {
const [editingId, setEditingId] = useState<string | null>(null)
const [editValue, setEditValue] = useState('')
const location = useLocation()
const updatePlan = useUpdatePlanFields()
// Confetti al llegar desde creación
useEffect(() => {
if (location.state.showConfetti) {
@@ -122,14 +122,47 @@ function DatosGeneralesPage() {
setEditValue('')
}
const handleSave = (id: string) => {
// Actualizamos el estado local de la lista
const handleSave = (campo: DatosGeneralesField) => {
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) =>
prev.map((c) => (c.id === id ? { ...c, value: editValue } : c)),
prev.map((c) => (c.id === campo.id ? { ...c, value: editValue } : c)),
)
setEditingId(null)
setEditValue('')
// toast.success('Cambios guardados localmente')
}
const handleIARequest = (clave: string) => {
@@ -245,7 +278,7 @@ function DatosGeneralesPage() {
<Button
size="sm"
className="bg-teal-600 hover:bg-teal-700"
onClick={() => handleSave(campo.id)}
onClick={() => handleSave(campo)}
>
<Check size={14} className="mr-1" /> Guardar
</Button>

View File

@@ -33,7 +33,7 @@ import {
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { usePlanAsignaturas, usePlanLineas } from '@/data'
import { usePlanAsignaturas, usePlanLineas, useUpdateAsignatura } from '@/data'
// --- Mapeadores (Fuera del componente para mayor limpieza) ---
const mapLineasToLineaCurricular = (
@@ -60,8 +60,9 @@ const mapAsignaturasToAsignaturas = (
tipo: asig.tipo === 'OBLIGATORIA' ? 'obligatoria' : 'optativa',
estado: 'borrador',
orden: asig.orden_celda ?? 0,
hd: Math.floor((asig.horas_semana ?? 0) / 2),
hi: Math.ceil((asig.horas_semana ?? 0) / 2),
// Mapeo directo de los nuevos campos de la API
hd: asig.horas_academicas ?? 0,
hi: asig.horas_independientes ?? 0,
prerrequisitos: [],
}))
}
@@ -157,10 +158,15 @@ function AsignaturaCardItem({
export const Route = createFileRoute('/planes/$planId/_detalle/mapa')({
component: MapaCurricularPage,
validateSearch: (search: { ciclo?: number }) => ({
ciclo: search.ciclo ?? null,
}),
})
function MapaCurricularPage() {
const { planId } = Route.useParams() // Idealmente usa el ID de la ruta
const { ciclo } = Route.useSearch()
console.log(ciclo)
// 1. Fetch de Datos
const { data: asignaturasApi, isLoading: loadingAsig } =
@@ -178,6 +184,7 @@ function MapaCurricularPage() {
useState<Asignatura | null>(null)
const [hasAreaComun, setHasAreaComun] = useState(false)
const [nombreNuevaLinea, setNombreNuevaLinea] = useState('') // Para el input de nombre personalizado
const { mutate: updateAsignatura, isPending } = useUpdateAsignatura()
const manejarAgregarLinea = (nombre: string) => {
const nombreNormalizado = nombre.trim()
@@ -249,7 +256,7 @@ function MapaCurricularPage() {
if (lineasApi) setLineas(mapLineasToLineaCurricular(lineasApi))
}, [lineasApi])
const ciclosTotales = 9
const ciclosTotales = Number(ciclo)
const ciclosArray = Array.from({ length: ciclosTotales }, (_, i) => i + 1)
// Nuevo estado para controlar los datos temporales del modal de edición
@@ -263,13 +270,41 @@ function MapaCurricularPage() {
setAsignaturas((prev) =>
prev.map((m) => (m.id === editingData.id ? { ...editingData } : m)),
)
// 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
// Cambiamos la condición: Mostramos la sección si hay asignaturas sin asignar
// 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 ---
const agregarLinea = (nombre: string) => {
@@ -304,7 +339,7 @@ function MapaCurricularPage() {
const getSubtotalLinea = (lineaId: string) => {
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(
(acc, m) => ({
cr: acc.cr + (m.creditos || 0),
@@ -327,6 +362,7 @@ function MapaCurricularPage() {
) => {
e.preventDefault()
if (draggedAsignatura) {
// 1. Actualización optimista del UI
setAsignaturas((prev) =>
prev.map((m) =>
m.id === draggedAsignatura
@@ -334,6 +370,23 @@ function MapaCurricularPage() {
: 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)
}
}
@@ -368,10 +421,15 @@ function MapaCurricularPage() {
</p>
</div>
<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">
<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>
)}
<DropdownMenu>
@@ -477,6 +535,8 @@ function MapaCurricularPage() {
<div
key={ciclo}
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)}
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 */}
<Dialog open={isEditModalOpen} onOpenChange={setIsEditModalOpen}>
<DialogContent className="sm:max-w-[550px]">
<DialogContent
className="sm:max-w-[550px]"
onInteractOutside={(e) => e.preventDefault()}
>
<DialogHeader>
<DialogTitle className="font-bold text-slate-700">
Editar Asignatura
@@ -734,32 +797,61 @@ function MapaCurricularPage() {
</div>
</div>
{/* Fila 4: Seriación (Igual a tu imagen) */}
{/* Fila 4: Seriación (Prerrequisitos) */}
<div className="space-y-2">
<label className="text-xs font-bold text-slate-500 uppercase">
Seriación (Prerrequisitos)
</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>
<SelectValue placeholder="Seleccionar asignatura..." />
</SelectTrigger>
<SelectContent>
{asignaturas.map((m) => (
{/* FILTRO CLAVE:
Solo mostramos materias cuyo ID sea diferente al de la materia que estamos editando
*/}
{asignaturas
.filter((m) => m.id !== editingData.id)
.map((m) => (
<SelectItem key={m.id} value={m.clave}>
{m.nombre}
{m.nombre} ({m.clave})
</SelectItem>
))}
</SelectContent>
</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) => (
<Badge
key={pre}
variant="secondary"
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>
))}
</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>
<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