1 Commits

10 changed files with 850 additions and 15 deletions

View File

@@ -41,7 +41,7 @@
"radix-ui": "^1.4.3",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"tailwind-merge": "^3.4.0",
"tailwind-merge": "^3.5.0",
"tailwindcss": "^4.0.6",
"tw-animate-css": "^1.3.6",
"use-debounce": "^10.1.0",
@@ -1327,7 +1327,7 @@
"symbol-tree": ["symbol-tree@3.2.4", "", {}, "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="],
"tailwind-merge": ["tailwind-merge@3.4.0", "", {}, "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g=="],
"tailwind-merge": ["tailwind-merge@3.5.0", "", {}, "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A=="],
"tailwindcss": ["tailwindcss@4.1.18", "", {}, "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw=="],

View File

@@ -54,7 +54,7 @@
"radix-ui": "^1.4.3",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"tailwind-merge": "^3.4.0",
"tailwind-merge": "^3.5.0",
"tailwindcss": "^4.0.6",
"tw-animate-css": "^1.3.6",
"use-debounce": "^10.1.0",

View File

@@ -0,0 +1,93 @@
import * as Icons from 'lucide-react'
import { useEffect, useRef } from 'react'
import type { NewSubjectWizardState } from '@/features/asignaturas/nueva/types'
import { PasoBasicosForm } from '@/components/asignaturas/wizard/PasoBasicosForm/PasoBasicosForm'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { useSubject } from '@/data'
export function PasoBasicosClonadoInterno({
wizard,
onChange,
}: {
wizard: NewSubjectWizardState
onChange: React.Dispatch<React.SetStateAction<NewSubjectWizardState>>
}) {
const sourceId = wizard.clonInterno?.asignaturaOrigenId ?? null
const { data: source, isLoading, isError } = useSubject(sourceId)
const lastAppliedRef = useRef<string | null>(null)
useEffect(() => {
if (!source) return
if (lastAppliedRef.current === source.id) return
lastAppliedRef.current = source.id
onChange((w) => ({
...w,
datosBasicos: {
...w.datosBasicos,
nombre: source.nombre,
codigo: source.codigo ?? '',
tipo: (source.tipo as any) ?? null,
creditos: source.creditos,
horasAcademicas: (source as any).horas_academicas ?? null,
horasIndependientes: (source as any).horas_independientes ?? null,
estructuraId: (source.estructura_id ??
w.datosBasicos.estructuraId) as any,
},
}))
}, [onChange, source])
if (!sourceId) {
return (
<Card>
<CardHeader>
<CardTitle className="text-base">Datos básicos</CardTitle>
</CardHeader>
<CardContent className="text-muted-foreground text-sm">
Selecciona una asignatura fuente para continuar.
</CardContent>
</Card>
)
}
if (isLoading) {
return (
<Card>
<CardHeader>
<CardTitle className="text-base">Datos básicos</CardTitle>
</CardHeader>
<CardContent className="text-muted-foreground text-sm">
Cargando información de la asignatura fuente
</CardContent>
</Card>
)
}
if (isError || !source) {
return (
<Card className="border-destructive/40">
<CardHeader>
<CardTitle className="text-destructive flex items-center gap-2 text-base">
<Icons.AlertTriangle className="h-5 w-5" />
No se pudo cargar la fuente
</CardTitle>
</CardHeader>
<CardContent className="text-muted-foreground text-sm">
Intenta seleccionar otra asignatura.
</CardContent>
</Card>
)
}
return (
<PasoBasicosForm
wizard={wizard}
onChange={onChange}
estructuraFuenteId={source.estructura_id ?? null}
/>
)
}

View File

@@ -1,3 +1,4 @@
import * as Icons from 'lucide-react'
import { useEffect, useState } from 'react'
import PasoSugerenciasForm from './PasoSugerenciasForm'
@@ -21,9 +22,11 @@ import { cn } from '@/lib/utils'
export function PasoBasicosForm({
wizard,
onChange,
estructuraFuenteId,
}: {
wizard: NewSubjectWizardState
onChange: React.Dispatch<React.SetStateAction<NewSubjectWizardState>>
estructuraFuenteId?: string | null
}) {
const { data: estructuras } = useSubjectEstructuras()
@@ -258,6 +261,17 @@ export function PasoBasicosForm({
)}
</SelectContent>
</Select>
{estructuraFuenteId &&
wizard.datosBasicos.estructuraId &&
wizard.datosBasicos.estructuraId !== estructuraFuenteId ? (
<div className="border-destructive/40 bg-destructive/5 text-destructive flex items-start gap-2 rounded-md border p-2 text-xs">
<Icons.AlertTriangle className="mt-0.5 h-4 w-4 flex-none" />
<span>
Es posible que se pierdan datos generales al seleccionar otra
estructura.
</span>
</div>
) : null}
<p className="text-muted-foreground text-xs">
Define los campos requeridos (ej. Objetivos, Temario, Evaluación).
</p>

View File

@@ -0,0 +1,360 @@
import { keepPreviousData, useQuery } from '@tanstack/react-query'
import * as Icons from 'lucide-react'
import { useEffect, useMemo } from 'react'
import { useDebounce } from 'use-debounce'
import type { NewSubjectWizardState } from '@/features/asignaturas/nueva/types'
import Pagination03 from '@/components/shadcn-studio/pagination/pagination-03'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { supabaseBrowser, useCatalogosPlanes, usePlanes } from '@/data'
import { cn } from '@/lib/utils'
type SourceSubjectRow = {
id: string
nombre: string
codigo: string | null
creditos: number
tipo: any
plan_estudio_id: string
estructura_id: string | null
}
const ALL = '__all__'
const normalizeLikeTerm = (term: string) =>
term.trim().replace(/[(),]/g, ' ').replace(/\s+/g, ' ')
export function PasoFuenteClonadoInterno({
wizard,
onChange,
}: {
wizard: NewSubjectWizardState
onChange: React.Dispatch<React.SetStateAction<NewSubjectWizardState>>
}) {
const pageSize = 20
const facultadId = wizard.clonInterno?.facultadId ?? null
const carreraId = wizard.clonInterno?.carreraId ?? null
const planOrigenId = wizard.clonInterno?.planOrigenId ?? null
const search = wizard.clonInterno?.search ?? ''
const page = Math.max(1, wizard.clonInterno?.page ?? 1)
const [debouncedSearch] = useDebounce(search, 350)
const { data: catalogos } = useCatalogosPlanes()
const carrerasOptions = useMemo(() => {
const raw = catalogos?.carreras ?? []
return facultadId ? raw.filter((c) => c.facultad_id === facultadId) : raw
}, [catalogos?.carreras, facultadId])
const planesQuery = usePlanes({
search: '',
facultadId: facultadId ?? 'todas',
carreraId: carreraId ?? 'todas',
estadoId: 'todos',
limit: 500,
offset: 0,
} as any)
const needPlansForFilter = Boolean((facultadId || carreraId) && !planOrigenId)
const plansForFilter = planesQuery.data?.data ?? []
const { data: subjectsPaged, isLoading: subjectsLoading } = useQuery({
queryKey: [
'asignaturas',
'clone-source',
{
facultadId,
carreraId,
planOrigenId,
search: debouncedSearch,
page,
pageSize,
planIdsKey: needPlansForFilter
? plansForFilter.map((p) => p.id).join(',')
: null,
},
],
enabled: !needPlansForFilter || !planesQuery.isLoading,
placeholderData: keepPreviousData,
queryFn: async () => {
const supabase = supabaseBrowser()
const from = (page - 1) * pageSize
const to = from + pageSize - 1
let q = supabase
.from('asignaturas')
.select(
'id,nombre,codigo,creditos,tipo,plan_estudio_id,estructura_id',
{
count: 'exact',
},
)
.order('nombre', { ascending: true })
if (planOrigenId) {
q = q.eq('plan_estudio_id', planOrigenId)
} else if (needPlansForFilter) {
const planIds = plansForFilter.map((p) => p.id)
if (!planIds.length) {
return { data: [] as Array<SourceSubjectRow>, count: 0 }
}
q = q.in('plan_estudio_id', planIds)
}
const term = normalizeLikeTerm(debouncedSearch)
if (term) {
// PostgREST OR syntax
q = q.or(`nombre.ilike.%${term}%,codigo.ilike.%${term}%`)
}
q = q.range(from, to)
const { data, error, count } = await q
if (error) throw new Error(error.message)
return {
data: data as unknown as Array<SourceSubjectRow>,
count: count ?? 0,
}
},
})
const subjects = subjectsPaged?.data ?? []
const total = subjectsPaged?.count ?? 0
const pageCount = Math.max(1, Math.ceil(total / pageSize))
useEffect(() => {
// clamp page if results shrink
if (page > pageCount) {
onChange((w) => ({
...w,
clonInterno: { ...(w.clonInterno ?? {}), page: pageCount },
}))
}
}, [onChange, page, pageCount])
const patchClonInterno = (
patch: Partial<NonNullable<NewSubjectWizardState['clonInterno']>>,
) =>
onChange((w) => ({
...w,
clonInterno: { ...(w.clonInterno ?? {}), ...patch },
}))
const hasAnyFilter = Boolean(
facultadId || carreraId || planOrigenId || search.trim().length,
)
const clearDisabled = !hasAnyFilter
const selectedId = wizard.clonInterno?.asignaturaOrigenId ?? null
const resetSelection = () => patchClonInterno({ asignaturaOrigenId: null })
return (
<div className="grid gap-4">
<Card>
<CardHeader>
<CardTitle className="text-base">Fuente</CardTitle>
</CardHeader>
<CardContent className="grid gap-4">
<div className="grid gap-3 sm:grid-cols-3">
<div className="grid gap-1">
<Label>Facultad</Label>
<Select
value={facultadId ?? ALL}
onValueChange={(val) => {
const next = val === ALL ? null : val
patchClonInterno({
facultadId: next,
carreraId: null,
planOrigenId: null,
page: 1,
})
resetSelection()
}}
>
<SelectTrigger>
<SelectValue placeholder="Todas" />
</SelectTrigger>
<SelectContent>
<SelectItem value={ALL}>Todas</SelectItem>
{(catalogos?.facultades ?? []).map((f) => (
<SelectItem key={f.id} value={f.id}>
{f.nombre}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="grid gap-1">
<Label>Carrera</Label>
<Select
value={carreraId ?? ALL}
onValueChange={(val) => {
const next = val === ALL ? null : val
patchClonInterno({
carreraId: next,
planOrigenId: null,
page: 1,
})
resetSelection()
}}
disabled={!facultadId}
>
<SelectTrigger>
<SelectValue
placeholder={facultadId ? 'Todas' : 'Selecciona facultad'}
/>
</SelectTrigger>
<SelectContent>
<SelectItem value={ALL}>Todas</SelectItem>
{carrerasOptions.map((c) => (
<SelectItem key={c.id} value={c.id}>
{c.nombre}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="grid gap-1">
<Label>Plan</Label>
<Select
value={planOrigenId ?? ALL}
onValueChange={(val) => {
const next = val === ALL ? null : val
patchClonInterno({ planOrigenId: next, page: 1 })
resetSelection()
}}
>
<SelectTrigger>
<SelectValue placeholder="Todos" />
</SelectTrigger>
<SelectContent>
<SelectItem value={ALL}>Todos</SelectItem>
{(planesQuery.data?.data ?? []).map((p) => (
<SelectItem key={p.id} value={p.id}>
{p.nombre}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<div className="grid gap-3 sm:grid-cols-[1fr_auto]">
<div className="grid gap-1">
<Label>Buscar</Label>
<Input
placeholder="Nombre o código..."
value={search}
onChange={(e) =>
patchClonInterno({ search: e.target.value, page: 1 })
}
/>
</div>
<div className="flex items-end justify-end">
<Button
type="button"
variant="secondary"
onClick={() => {
patchClonInterno({
facultadId: null,
carreraId: null,
planOrigenId: null,
search: '',
page: 1,
asignaturaOrigenId: null,
})
}}
disabled={clearDisabled}
>
<Icons.X className="mr-2 h-4 w-4" />
Limpiar filtros
</Button>
</div>
</div>
</CardContent>
</Card>
<div className="grid gap-2">
<div className="text-muted-foreground text-xs">
Selecciona una asignatura fuente (solo una).
</div>
<div className="grid max-h-80 gap-2 overflow-y-auto">
{subjectsLoading ? (
<div className="text-muted-foreground text-sm">
Cargando asignaturas
</div>
) : subjects.length === 0 ? (
<div className="text-muted-foreground text-sm">
No hay asignaturas con esos filtros.
</div>
) : (
subjects.map((m) => {
const active = String(selectedId) === String(m.id)
return (
<label
key={m.id}
className={cn(
'hover:bg-accent flex cursor-pointer items-center justify-between rounded-md border p-3 text-left',
active && 'border-primary bg-primary/5 ring-primary ring-1',
)}
>
<input
className="sr-only"
type="radio"
name="asignaturaFuente"
checked={active}
onChange={() =>
patchClonInterno({ asignaturaOrigenId: m.id, page })
}
/>
<div className="min-w-0">
<div className="truncate font-medium">{m.nombre}</div>
<div className="text-muted-foreground mt-0.5 text-xs">
{(m.codigo ? m.codigo : '—') +
' • ' +
String(m.creditos) +
' créditos'}
</div>
</div>
{active ? (
<Icons.CheckCircle2 className="text-primary h-5 w-5 flex-none" />
) : (
<span className="h-5 w-5 flex-none" aria-hidden />
)}
</label>
)
})
)}
</div>
{pageCount > 1 ? (
<Pagination03
page={page}
pageCount={pageCount}
onPageChange={(nextPage) => patchClonInterno({ page: nextPage })}
/>
) : null}
</div>
</div>
)
}

View File

@@ -15,6 +15,7 @@ import {
qk,
useCreateSubjectManual,
subjects_get_maybe,
subjects_get,
} from '@/data'
export function WizardControls({
@@ -201,6 +202,85 @@ export function WizardControls({
let startedWaiting = false
try {
if (wizard.tipoOrigen === 'CLONADO_INTERNO') {
if (!wizard.plan_estudio_id) {
throw new Error('Plan de estudio inválido.')
}
const asignaturaOrigenId = wizard.clonInterno?.asignaturaOrigenId
if (!asignaturaOrigenId) {
throw new Error('Selecciona una asignatura fuente.')
}
if (!wizard.datosBasicos.estructuraId) {
throw new Error('Estructura inválida.')
}
if (!wizard.datosBasicos.nombre.trim()) {
throw new Error('Nombre inválido.')
}
if (wizard.datosBasicos.tipo == null) {
throw new Error('Tipo inválido.')
}
if (wizard.datosBasicos.creditos == null) {
throw new Error('Créditos inválidos.')
}
const fuente = await subjects_get(asignaturaOrigenId as any)
const supabase = supabaseBrowser()
const codigo = (wizard.datosBasicos.codigo ?? '').trim()
const payload: TablesInsert<'asignaturas'> = {
plan_estudio_id: wizard.plan_estudio_id,
estructura_id: wizard.datosBasicos.estructuraId,
codigo: codigo ? codigo : null,
nombre: wizard.datosBasicos.nombre,
tipo: wizard.datosBasicos.tipo,
creditos: wizard.datosBasicos.creditos,
datos: (fuente as any).datos,
contenido_tematico: (fuente as any).contenido_tematico,
criterios_de_evaluacion: (fuente as any).criterios_de_evaluacion,
tipo_origen: 'CLONADO_INTERNO',
meta_origen: {
...(fuente as any).meta_origen,
asignatura_origen_id: fuente.id,
plan_origen_id: (fuente as any).plan_estudio_id,
},
horas_academicas:
wizard.datosBasicos.horasAcademicas ??
(fuente as any).horas_academicas ??
null,
horas_independientes:
wizard.datosBasicos.horasIndependientes ??
(fuente as any).horas_independientes ??
null,
}
const { data: inserted, error: insertError } = await supabase
.from('asignaturas')
.insert(payload)
.select('id,plan_estudio_id')
.single()
if (insertError) throw new Error(insertError.message)
qc.invalidateQueries({
queryKey: qk.planAsignaturas(wizard.plan_estudio_id),
})
qc.invalidateQueries({
queryKey: qk.planHistorial(wizard.plan_estudio_id),
})
navigate({
to: `/planes/${inserted.plan_estudio_id}/asignaturas/${inserted.id}`,
state: { showConfetti: true },
resetScroll: false,
})
return
}
if (wizard.tipoOrigen === 'IA_SIMPLE') {
if (!wizard.plan_estudio_id) {
throw new Error('Plan de estudio inválido.')

View File

@@ -0,0 +1,128 @@
import { buttonVariants } from '@/components/ui/button'
import {
Pagination,
PaginationContent,
PaginationEllipsis,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious,
} from '@/components/ui/pagination'
import { cn } from '@/lib/utils'
export type Pagination03Props = {
page: number
pageCount: number
onPageChange: (page: number) => void
className?: string
}
const toInt = (n: unknown, fallback: number) => {
const x = typeof n === 'number' ? n : Number(n)
return Number.isFinite(x) ? Math.floor(x) : fallback
}
function getPageItems(page: number, pageCount: number): Array<number | '...'> {
if (pageCount <= 7) {
return Array.from({ length: pageCount }, (_, i) => i + 1)
}
const items: Array<number | '...'> = []
const safePage = Math.min(Math.max(page, 1), pageCount)
items.push(1)
const start = Math.max(2, safePage - 1)
const end = Math.min(pageCount - 1, safePage + 1)
if (start > 2) items.push('...')
for (let p = start; p <= end; p++) items.push(p)
if (end < pageCount - 1) items.push('...')
items.push(pageCount)
return items
}
export function Pagination03({
page,
pageCount,
onPageChange,
className,
}: Pagination03Props) {
const safePageCount = Math.max(1, toInt(pageCount, 1))
const safePage = Math.min(Math.max(toInt(page, 1), 1), safePageCount)
const items = getPageItems(safePage, safePageCount)
const canPrev = safePage > 1
const canNext = safePage < safePageCount
const go = (p: number) => {
const next = Math.min(Math.max(p, 1), safePageCount)
if (next === safePage) return
onPageChange(next)
}
return (
<Pagination className={className}>
<PaginationContent>
<PaginationItem>
<PaginationPrevious
href="#"
className={cn(!canPrev && 'pointer-events-none opacity-50')}
onClick={(e) => {
e.preventDefault()
if (!canPrev) return
go(safePage - 1)
}}
/>
</PaginationItem>
{items.map((it, idx) =>
it === '...' ? (
<PaginationItem key={`ellipsis-${idx}`}>
<PaginationEllipsis />
</PaginationItem>
) : (
<PaginationItem key={it}>
<PaginationLink
href="#"
isActive={it === safePage}
onClick={(e) => {
e.preventDefault()
go(it)
}}
className={
it === safePage
? cn(
buttonVariants({
variant: 'default',
size: 'icon',
}),
'hover:text-primary-foreground! dark:bg-primary dark:text-primary-foreground dark:hover:text-primary-foreground dark:hover:bg-primary/90 shadow-none! dark:border-transparent',
)
: undefined
}
>
{it}
</PaginationLink>
</PaginationItem>
),
)}
<PaginationItem>
<PaginationNext
href="#"
className={cn(!canNext && 'pointer-events-none opacity-50')}
onClick={(e) => {
e.preventDefault()
if (!canNext) return
go(safePage + 1)
}}
/>
</PaginationItem>
</PaginationContent>
</Pagination>
)
}
export default Pagination03

View File

@@ -0,0 +1,127 @@
import * as React from "react"
import {
ChevronLeftIcon,
ChevronRightIcon,
MoreHorizontalIcon,
} from "lucide-react"
import { cn } from "@/lib/utils"
import { buttonVariants, type Button } from "@/components/ui/button"
function Pagination({ className, ...props }: React.ComponentProps<"nav">) {
return (
<nav
role="navigation"
aria-label="pagination"
data-slot="pagination"
className={cn("mx-auto flex w-full justify-center", className)}
{...props}
/>
)
}
function PaginationContent({
className,
...props
}: React.ComponentProps<"ul">) {
return (
<ul
data-slot="pagination-content"
className={cn("flex flex-row items-center gap-1", className)}
{...props}
/>
)
}
function PaginationItem({ ...props }: React.ComponentProps<"li">) {
return <li data-slot="pagination-item" {...props} />
}
type PaginationLinkProps = {
isActive?: boolean
} & Pick<React.ComponentProps<typeof Button>, "size"> &
React.ComponentProps<"a">
function PaginationLink({
className,
isActive,
size = "icon",
...props
}: PaginationLinkProps) {
return (
<a
aria-current={isActive ? "page" : undefined}
data-slot="pagination-link"
data-active={isActive}
className={cn(
buttonVariants({
variant: isActive ? "outline" : "ghost",
size,
}),
className
)}
{...props}
/>
)
}
function PaginationPrevious({
className,
...props
}: React.ComponentProps<typeof PaginationLink>) {
return (
<PaginationLink
aria-label="Go to previous page"
size="default"
className={cn("gap-1 px-2.5 sm:pl-2.5", className)}
{...props}
>
<ChevronLeftIcon />
<span className="hidden sm:block">Previous</span>
</PaginationLink>
)
}
function PaginationNext({
className,
...props
}: React.ComponentProps<typeof PaginationLink>) {
return (
<PaginationLink
aria-label="Go to next page"
size="default"
className={cn("gap-1 px-2.5 sm:pr-2.5", className)}
{...props}
>
<span className="hidden sm:block">Next</span>
<ChevronRightIcon />
</PaginationLink>
)
}
function PaginationEllipsis({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
aria-hidden
data-slot="pagination-ellipsis"
className={cn("flex size-9 items-center justify-center", className)}
{...props}
>
<MoreHorizontalIcon className="size-4" />
<span className="sr-only">More pages</span>
</span>
)
}
export {
Pagination,
PaginationContent,
PaginationLink,
PaginationItem,
PaginationPrevious,
PaginationNext,
PaginationEllipsis,
}

View File

@@ -3,8 +3,10 @@ import * as Icons from 'lucide-react'
import { useNuevaAsignaturaWizard } from './hooks/useNuevaAsignaturaWizard'
import { PasoBasicosClonadoInterno } from '@/components/asignaturas/wizard/PasoBasicosClonadoInterno.tsx'
import { PasoBasicosForm } from '@/components/asignaturas/wizard/PasoBasicosForm/PasoBasicosForm'
import { PasoDetallesPanel } from '@/components/asignaturas/wizard/PasoDetallesPanel'
import { PasoFuenteClonadoInterno } from '@/components/asignaturas/wizard/PasoFuenteClonadoInterno.tsx'
import { PasoMetodoCardGroup } from '@/components/asignaturas/wizard/PasoMetodoCardGroup'
import { PasoResumenCard } from '@/components/asignaturas/wizard/PasoResumenCard'
import { WizardControls } from '@/components/asignaturas/wizard/WizardControls'
@@ -63,7 +65,12 @@ export function NuevaAsignaturaModalContainer({ planId }: { planId: string }) {
basicos: 'Sugerencias',
detalles: 'Estructura',
}
: undefined
: wizard.tipoOrigen === 'CLONADO_INTERNO'
? {
basicos: 'Fuente',
detalles: 'Datos básicos',
}
: undefined
const handleClose = () => {
navigate({ to: `/planes/${planId}/asignaturas`, resetScroll: false })
@@ -99,6 +106,21 @@ export function NuevaAsignaturaModalContainer({ planId }: { planId: string }) {
>
{({ methods }) => {
const idx = Wizard.utils.getIndex(methods.current.id)
const stepId = methods.current.id
const disableNext =
wizard.isLoading ||
(stepId === 'metodo'
? !canContinueDesdeMetodo
: stepId === 'basicos'
? wizard.tipoOrigen === 'CLONADO_INTERNO'
? !canContinueDesdeDetalles
: !canContinueDesdeBasicos
: stepId === 'detalles'
? wizard.tipoOrigen === 'CLONADO_INTERNO'
? !canContinueDesdeBasicos
: !canContinueDesdeDetalles
: false)
return (
<WizardLayout
@@ -118,12 +140,7 @@ export function NuevaAsignaturaModalContainer({ planId }: { planId: string }) {
onPrev={() => methods.prev()}
onNext={() => methods.next()}
disablePrev={idx === 0 || wizard.isLoading}
disableNext={
wizard.isLoading ||
(idx === 0 && !canContinueDesdeMetodo) ||
(idx === 1 && !canContinueDesdeBasicos) ||
(idx === 2 && !canContinueDesdeDetalles)
}
disableNext={disableNext}
disableCreate={wizard.isLoading}
isLastStep={idx >= Wizard.steps.length - 1}
wizard={wizard}
@@ -141,13 +158,27 @@ export function NuevaAsignaturaModalContainer({ planId }: { planId: string }) {
{idx === 1 && (
<Wizard.Stepper.Panel>
<PasoBasicosForm wizard={wizard} onChange={setWizard} />
{wizard.tipoOrigen === 'CLONADO_INTERNO' ? (
<PasoFuenteClonadoInterno
wizard={wizard}
onChange={setWizard}
/>
) : (
<PasoBasicosForm wizard={wizard} onChange={setWizard} />
)}
</Wizard.Stepper.Panel>
)}
{idx === 2 && (
<Wizard.Stepper.Panel>
<PasoDetallesPanel wizard={wizard} onChange={setWizard} />
{wizard.tipoOrigen === 'CLONADO_INTERNO' ? (
<PasoBasicosClonadoInterno
wizard={wizard}
onChange={setWizard}
/>
) : (
<PasoDetallesPanel wizard={wizard} onChange={setWizard} />
)}
</Wizard.Stepper.Panel>
)}

View File

@@ -50,10 +50,12 @@ export type NewSubjectWizardState = {
}
sugerencias: Array<AsignaturaSugerida>
clonInterno?: {
facultadId?: string
carreraId?: string
planOrigenId?: string
facultadId?: string | null
carreraId?: string | null
planOrigenId?: string | null
asignaturaOrigenId?: string | null
search?: string
page?: number
}
clonTradicional?: {
archivoWordAsignaturaId: string | null