issue/63-marca-edge-function-returned-a-non-2xx-status-code #119
1
.gitignore
vendored
1
.gitignore
vendored
@@ -8,3 +8,4 @@ count.txt
|
||||
.nitro
|
||||
.tanstack
|
||||
.wrangler
|
||||
diff.txt
|
||||
@@ -1,7 +1,7 @@
|
||||
import type {
|
||||
NewSubjectWizardState,
|
||||
TipoAsignatura,
|
||||
} from '@/features/asignaturas/nueva/types'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
import type { NewSubjectWizardState } from '@/features/asignaturas/nueva/types'
|
||||
import type { Database } from '@/types/supabase'
|
||||
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
@@ -12,10 +12,9 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
import {
|
||||
ESTRUCTURAS_SEP,
|
||||
TIPOS_MATERIA,
|
||||
} from '@/features/asignaturas/nueva/catalogs'
|
||||
import { useSubjectEstructuras } from '@/data'
|
||||
import { TIPOS_MATERIA } from '@/features/asignaturas/nueva/catalogs'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
export function PasoBasicosForm({
|
||||
wizard,
|
||||
@@ -24,6 +23,20 @@ export function PasoBasicosForm({
|
||||
wizard: NewSubjectWizardState
|
||||
onChange: React.Dispatch<React.SetStateAction<NewSubjectWizardState>>
|
||||
}) {
|
||||
const { data: estructuras } = useSubjectEstructuras()
|
||||
|
||||
const [creditosInput, setCreditosInput] = useState<string>(() => {
|
||||
const c = Number(wizard.datosBasicos.creditos ?? 0)
|
||||
return c > 0 ? c.toFixed(2) : ''
|
||||
})
|
||||
const [creditosFocused, setCreditosFocused] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (creditosFocused) return
|
||||
const c = Number(wizard.datosBasicos.creditos ?? 0)
|
||||
setCreditosInput(c > 0 ? c.toFixed(2) : '')
|
||||
}, [wizard.datosBasicos.creditos, creditosFocused])
|
||||
|
||||
return (
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<div className="grid gap-1 sm:col-span-2">
|
||||
@@ -33,45 +46,66 @@ export function PasoBasicosForm({
|
||||
placeholder="Ej. Matemáticas Discretas"
|
||||
value={wizard.datosBasicos.nombre}
|
||||
onChange={(e) =>
|
||||
onChange((w) => ({
|
||||
onChange(
|
||||
(w): NewSubjectWizardState => ({
|
||||
...w,
|
||||
datosBasicos: { ...w.datosBasicos, nombre: e.target.value },
|
||||
}))
|
||||
}),
|
||||
)
|
||||
}
|
||||
className="placeholder:text-muted-foreground/70 font-medium not-italic placeholder:font-normal placeholder:italic"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-1">
|
||||
<Label htmlFor="clave">Clave (Opcional)</Label>
|
||||
<Label htmlFor="codigo">
|
||||
Código
|
||||
<span className="text-xs font-normal text-gray-500 dark:text-gray-400">
|
||||
(Opcional)
|
||||
</span>
|
||||
</Label>
|
||||
<Input
|
||||
id="clave"
|
||||
id="codigo"
|
||||
placeholder="Ej. MAT-101"
|
||||
value={wizard.datosBasicos.clave || ''}
|
||||
value={wizard.datosBasicos.codigo || ''}
|
||||
onChange={(e) =>
|
||||
onChange((w) => ({
|
||||
onChange(
|
||||
(w): NewSubjectWizardState => ({
|
||||
...w,
|
||||
datosBasicos: { ...w.datosBasicos, clave: e.target.value },
|
||||
}))
|
||||
datosBasicos: { ...w.datosBasicos, codigo: e.target.value },
|
||||
}),
|
||||
)
|
||||
}
|
||||
className="placeholder:text-muted-foreground/70 placeholder:italicplaceholder:text-muted-foreground/70 font-medium not-italic placeholder:font-normal placeholder:italic"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-1">
|
||||
<Label htmlFor="tipo">Tipo</Label>
|
||||
<Select
|
||||
value={wizard.datosBasicos.tipo}
|
||||
onValueChange={(val) =>
|
||||
onChange((w) => ({
|
||||
value={(wizard.datosBasicos.tipo ?? '') as string}
|
||||
onValueChange={(value: string) =>
|
||||
onChange(
|
||||
(w): NewSubjectWizardState => ({
|
||||
...w,
|
||||
datosBasicos: { ...w.datosBasicos, tipo: val as TipoAsignatura },
|
||||
}))
|
||||
datosBasicos: {
|
||||
...w.datosBasicos,
|
||||
tipo: value as NewSubjectWizardState['datosBasicos']['tipo'],
|
||||
},
|
||||
}),
|
||||
)
|
||||
}
|
||||
>
|
||||
<SelectTrigger
|
||||
id="tipo"
|
||||
className="w-full min-w-0 [&>span]:block! [&>span]:truncate!"
|
||||
className={cn(
|
||||
'w-full min-w-0 [&>span]:block! [&>span]:truncate!',
|
||||
!wizard.datosBasicos.tipo
|
||||
? 'text-muted-foreground font-normal italic opacity-70'
|
||||
: 'font-medium not-italic',
|
||||
)}
|
||||
>
|
||||
<SelectValue />
|
||||
<SelectValue placeholder="Ej. Obligatoria" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{TIPOS_MATERIA.map((t) => (
|
||||
@@ -87,49 +121,175 @@ export function PasoBasicosForm({
|
||||
<Label htmlFor="creditos">Créditos</Label>
|
||||
<Input
|
||||
id="creditos"
|
||||
type="number"
|
||||
min={0}
|
||||
value={wizard.datosBasicos.creditos}
|
||||
onChange={(e) =>
|
||||
type="text"
|
||||
inputMode="decimal"
|
||||
pattern="^\\d*(?:[.,]\\d{0,2})?$"
|
||||
value={creditosInput}
|
||||
onKeyDown={(e) => {
|
||||
if (['-', 'e', 'E', '+'].includes(e.key)) {
|
||||
e.preventDefault()
|
||||
}
|
||||
}}
|
||||
onFocus={() => setCreditosFocused(true)}
|
||||
onBlur={() => {
|
||||
setCreditosFocused(false)
|
||||
|
||||
const raw = creditosInput.trim()
|
||||
if (!raw) {
|
||||
onChange((w) => ({
|
||||
...w,
|
||||
datosBasicos: { ...w.datosBasicos, creditos: 0 },
|
||||
}))
|
||||
return
|
||||
}
|
||||
|
||||
const normalized = raw.replace(',', '.')
|
||||
const asNumber = Number.parseFloat(normalized)
|
||||
if (!Number.isFinite(asNumber) || asNumber <= 0) {
|
||||
setCreditosInput('')
|
||||
onChange((w) => ({
|
||||
...w,
|
||||
datosBasicos: { ...w.datosBasicos, creditos: 0 },
|
||||
}))
|
||||
return
|
||||
}
|
||||
|
||||
const fixed = asNumber.toFixed(2)
|
||||
setCreditosInput(fixed)
|
||||
onChange((w) => ({
|
||||
...w,
|
||||
datosBasicos: { ...w.datosBasicos, creditos: Number(fixed) },
|
||||
}))
|
||||
}}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const nextRaw = e.target.value
|
||||
if (nextRaw === '') {
|
||||
setCreditosInput('')
|
||||
onChange((w) => ({
|
||||
...w,
|
||||
datosBasicos: { ...w.datosBasicos, creditos: 0 },
|
||||
}))
|
||||
return
|
||||
}
|
||||
|
||||
if (!/^\d*(?:[.,]\d{0,2})?$/.test(nextRaw)) return
|
||||
|
||||
setCreditosInput(nextRaw)
|
||||
|
||||
const asNumber = Number.parseFloat(nextRaw.replace(',', '.'))
|
||||
onChange((w) => ({
|
||||
...w,
|
||||
datosBasicos: {
|
||||
...w.datosBasicos,
|
||||
creditos: Number(e.target.value || 0),
|
||||
creditos:
|
||||
Number.isFinite(asNumber) && asNumber > 0 ? asNumber : 0,
|
||||
},
|
||||
}))
|
||||
}
|
||||
}}
|
||||
className="placeholder:text-muted-foreground/70 font-medium not-italic placeholder:font-normal placeholder:italic"
|
||||
placeholder="Ej. 4.50"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-1">
|
||||
<Label htmlFor="horas">Horas / Semana</Label>
|
||||
<Label htmlFor="horasAcademicas">
|
||||
Horas Académicas
|
||||
<span className="text-xs font-normal text-gray-500 dark:text-gray-400">
|
||||
(Opcional)
|
||||
</span>
|
||||
</Label>
|
||||
<Input
|
||||
id="horas"
|
||||
id="horasAcademicas"
|
||||
type="number"
|
||||
min={0}
|
||||
value={wizard.datosBasicos.horasSemana || 0}
|
||||
onChange={(e) =>
|
||||
onChange((w) => ({
|
||||
min={1}
|
||||
step={1}
|
||||
inputMode="numeric"
|
||||
pattern="[0-9]*"
|
||||
value={wizard.datosBasicos.horasAcademicas ?? ''}
|
||||
onKeyDown={(e) => {
|
||||
if (['.', ',', '-', 'e', 'E', '+'].includes(e.key)) {
|
||||
e.preventDefault()
|
||||
}
|
||||
}}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
onChange(
|
||||
(w): NewSubjectWizardState => ({
|
||||
...w,
|
||||
datosBasicos: {
|
||||
...w.datosBasicos,
|
||||
horasSemana: Number(e.target.value || 0),
|
||||
horasAcademicas: (() => {
|
||||
const raw = e.target.value
|
||||
if (raw === '') return null
|
||||
const asNumber = Number(raw)
|
||||
if (Number.isNaN(asNumber)) return null
|
||||
// Coerce to positive integer (natural numbers without zero)
|
||||
const n = Math.floor(Math.abs(asNumber))
|
||||
return n >= 1 ? n : 1
|
||||
})(),
|
||||
},
|
||||
}))
|
||||
}),
|
||||
)
|
||||
}
|
||||
className="placeholder:text-muted-foreground/70 font-medium not-italic placeholder:font-normal placeholder:italic"
|
||||
placeholder="Ej. 48"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-1 sm:col-span-2">
|
||||
<div className="grid gap-1">
|
||||
<Label htmlFor="horasIndependientes">
|
||||
Horas Independientes
|
||||
<span className="text-xs font-normal text-gray-500 dark:text-gray-400">
|
||||
(Opcional)
|
||||
</span>
|
||||
</Label>
|
||||
<Input
|
||||
id="horasIndependientes"
|
||||
type="number"
|
||||
min={1}
|
||||
step={1}
|
||||
inputMode="numeric"
|
||||
pattern="[0-9]*"
|
||||
value={wizard.datosBasicos.horasIndependientes ?? ''}
|
||||
onKeyDown={(e) => {
|
||||
if (['.', ',', '-', 'e', 'E', '+'].includes(e.key)) {
|
||||
e.preventDefault()
|
||||
}
|
||||
}}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
onChange(
|
||||
(w): NewSubjectWizardState => ({
|
||||
...w,
|
||||
datosBasicos: {
|
||||
...w.datosBasicos,
|
||||
horasIndependientes: (() => {
|
||||
const raw = e.target.value
|
||||
if (raw === '') return null
|
||||
const asNumber = Number(raw)
|
||||
if (Number.isNaN(asNumber)) return null
|
||||
// Coerce to positive integer (natural numbers without zero)
|
||||
const n = Math.floor(Math.abs(asNumber))
|
||||
return n >= 1 ? n : 1
|
||||
})(),
|
||||
},
|
||||
}),
|
||||
)
|
||||
}
|
||||
className="placeholder:text-muted-foreground/70 font-medium not-italic placeholder:font-normal placeholder:italic"
|
||||
placeholder="Ej. 24"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-1">
|
||||
<Label htmlFor="estructura">Estructura de la asignatura</Label>
|
||||
<Select
|
||||
value={wizard.datosBasicos.estructuraId}
|
||||
value={wizard.datosBasicos.estructuraId as string}
|
||||
onValueChange={(val) =>
|
||||
onChange((w) => ({
|
||||
onChange(
|
||||
(w): NewSubjectWizardState => ({
|
||||
...w,
|
||||
datosBasicos: { ...w.datosBasicos, estructuraId: val },
|
||||
}))
|
||||
}),
|
||||
)
|
||||
}
|
||||
>
|
||||
<SelectTrigger
|
||||
@@ -139,11 +299,15 @@ export function PasoBasicosForm({
|
||||
<SelectValue placeholder="Selecciona plantilla..." />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{ESTRUCTURAS_SEP.map((e) => (
|
||||
{estructuras?.map(
|
||||
(
|
||||
e: Database['public']['Tables']['estructuras_asignatura']['Row'],
|
||||
) => (
|
||||
<SelectItem key={e.id} value={e.id}>
|
||||
{e.label}
|
||||
{e.nombre}
|
||||
</SelectItem>
|
||||
))}
|
||||
),
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import * as Icons from 'lucide-react'
|
||||
|
||||
import type { UploadedFile } from '@/components/planes/wizard/PasoDetallesPanel/FileDropZone'
|
||||
import type { NewSubjectWizardState } from '@/features/asignaturas/nueva/types'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import ReferenciasParaIA from '@/components/planes/wizard/PasoDetallesPanel/ReferenciasParaIA'
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
@@ -21,22 +21,21 @@ import {
|
||||
} from '@/components/ui/select'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import {
|
||||
ARCHIVOS_SISTEMA_MOCK,
|
||||
FACULTADES,
|
||||
MATERIAS_MOCK,
|
||||
PLANES_MOCK,
|
||||
} from '@/features/asignaturas/nueva/catalogs'
|
||||
|
||||
export function PasoConfiguracionPanel({
|
||||
export function PasoDetallesPanel({
|
||||
wizard,
|
||||
onChange,
|
||||
onGenerarIA,
|
||||
onGenerarIA: _onGenerarIA,
|
||||
}: {
|
||||
wizard: NewSubjectWizardState
|
||||
onChange: React.Dispatch<React.SetStateAction<NewSubjectWizardState>>
|
||||
onGenerarIA: () => void
|
||||
}) {
|
||||
if (wizard.modoCreacion === 'MANUAL') {
|
||||
if (wizard.tipoOrigen === 'MANUAL') {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
@@ -50,116 +49,104 @@ export function PasoConfiguracionPanel({
|
||||
)
|
||||
}
|
||||
|
||||
if (wizard.modoCreacion === 'IA') {
|
||||
if (wizard.tipoOrigen === 'IA') {
|
||||
return (
|
||||
<div className="grid gap-4">
|
||||
<div className="grid gap-1">
|
||||
<Label>Descripción del enfoque</Label>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-1">
|
||||
<Label>Descripción del enfoque académico</Label>
|
||||
<Textarea
|
||||
placeholder="Ej. Asignatura teórica-práctica enfocada en patrones de diseño..."
|
||||
value={wizard.iaConfig?.descripcionEnfoque}
|
||||
placeholder="Describe el enfoque, alcance y público objetivo. Ej.: Teórica-práctica enfocada en patrones de diseño, con proyectos semanales."
|
||||
value={wizard.iaConfig?.descripcionEnfoqueAcademico}
|
||||
onChange={(e) =>
|
||||
onChange((w) => ({
|
||||
onChange(
|
||||
(w): NewSubjectWizardState => ({
|
||||
...w,
|
||||
iaConfig: {
|
||||
...w.iaConfig!,
|
||||
descripcionEnfoque: e.target.value,
|
||||
descripcionEnfoqueAcademico: e.target.value,
|
||||
},
|
||||
}))
|
||||
}),
|
||||
)
|
||||
}
|
||||
className="min-h-25"
|
||||
className="placeholder:text-muted-foreground/70 min-h-25 font-medium not-italic placeholder:font-normal placeholder:italic"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-1">
|
||||
<Label>Notas adicionales</Label>
|
||||
<Textarea
|
||||
placeholder="Restricciones, bibliografía sugerida, etc."
|
||||
value={wizard.iaConfig?.notasAdicionales}
|
||||
onChange={(e) =>
|
||||
onChange((w) => ({
|
||||
...w,
|
||||
iaConfig: { ...w.iaConfig!, notasAdicionales: e.target.value },
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label>Archivos de contexto (Opcional)</Label>
|
||||
<div className="flex flex-col gap-2 rounded-md border p-3">
|
||||
{ARCHIVOS_SISTEMA_MOCK.map((file) => (
|
||||
<div key={file.id} className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
id={file.id}
|
||||
checked={wizard.iaConfig?.archivosExistentesIds.includes(
|
||||
file.id,
|
||||
)}
|
||||
onChange={(e) => {
|
||||
const checked = e.target.checked
|
||||
onChange((w) => ({
|
||||
...w,
|
||||
iaConfig: {
|
||||
...w.iaConfig!,
|
||||
archivosExistentesIds: checked
|
||||
? [
|
||||
...(w.iaConfig?.archivosExistentesIds || []),
|
||||
file.id,
|
||||
]
|
||||
: w.iaConfig?.archivosExistentesIds.filter(
|
||||
(id) => id !== file.id,
|
||||
) || [],
|
||||
},
|
||||
}))
|
||||
}}
|
||||
/>
|
||||
<Label htmlFor={file.id} className="font-normal">
|
||||
{file.name}
|
||||
<div className="flex flex-col gap-1">
|
||||
<Label>
|
||||
Instrucciones adicionales para la IA
|
||||
<span className="text-xs font-normal text-gray-500 dark:text-gray-400">
|
||||
(Opcional)
|
||||
</span>
|
||||
</Label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<Textarea
|
||||
placeholder="Opcional: restricciones y preferencias. Ej.: incluye bibliografía en español, evita contenido avanzado, prioriza evaluación por proyectos."
|
||||
value={wizard.iaConfig?.instruccionesAdicionalesIA}
|
||||
onChange={(e) =>
|
||||
onChange(
|
||||
(w): NewSubjectWizardState => ({
|
||||
...w,
|
||||
iaConfig: {
|
||||
...w.iaConfig!,
|
||||
instruccionesAdicionalesIA: e.target.value,
|
||||
},
|
||||
}),
|
||||
)
|
||||
}
|
||||
className="placeholder:text-muted-foreground/70 font-medium not-italic placeholder:font-normal placeholder:italic"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button onClick={onGenerarIA} disabled={wizard.isLoading}>
|
||||
{wizard.isLoading ? (
|
||||
<>
|
||||
<Icons.Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Generando...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Icons.Sparkles className="mr-2 h-4 w-4" /> Generar Preview
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{wizard.resumen.previewAsignatura && (
|
||||
<Card className="bg-muted/50 border-dashed">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">Vista previa generada</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="text-muted-foreground text-sm">
|
||||
<p>
|
||||
<strong>Objetivo:</strong>{' '}
|
||||
{wizard.resumen.previewAsignatura.objetivo}
|
||||
</p>
|
||||
<p className="mt-2">
|
||||
Se detectaron {wizard.resumen.previewAsignatura.unidades}{' '}
|
||||
unidades temáticas y{' '}
|
||||
{wizard.resumen.previewAsignatura.bibliografiaCount} fuentes
|
||||
bibliográficas.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
<ReferenciasParaIA
|
||||
selectedArchivoIds={wizard.iaConfig?.archivosReferencia || []}
|
||||
selectedRepositorioIds={wizard.iaConfig?.repositoriosReferencia || []}
|
||||
uploadedFiles={wizard.iaConfig?.archivosAdjuntos || []}
|
||||
onToggleArchivo={(id, checked) =>
|
||||
onChange((w): NewSubjectWizardState => {
|
||||
const prev = w.iaConfig?.archivosReferencia || []
|
||||
const next = checked
|
||||
? [...prev, id]
|
||||
: prev.filter((a) => a !== id)
|
||||
return {
|
||||
...w,
|
||||
iaConfig: {
|
||||
...w.iaConfig!,
|
||||
archivosReferencia: next,
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
onToggleRepositorio={(id, checked) =>
|
||||
onChange((w): NewSubjectWizardState => {
|
||||
const prev = w.iaConfig?.repositoriosReferencia || []
|
||||
const next = checked
|
||||
? [...prev, id]
|
||||
: prev.filter((r) => r !== id)
|
||||
return {
|
||||
...w,
|
||||
iaConfig: {
|
||||
...w.iaConfig!,
|
||||
repositoriosReferencia: next,
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
onFilesChange={(files: Array<UploadedFile>) =>
|
||||
onChange(
|
||||
(w): NewSubjectWizardState => ({
|
||||
...w,
|
||||
iaConfig: {
|
||||
...w.iaConfig!,
|
||||
archivosAdjuntos: files,
|
||||
},
|
||||
}),
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (wizard.subModoClonado === 'INTERNO') {
|
||||
if (wizard.tipoOrigen === 'CLONADO_INTERNO') {
|
||||
return (
|
||||
<div className="grid gap-4">
|
||||
<div className="grid gap-2 sm:grid-cols-3">
|
||||
@@ -217,12 +204,22 @@ export function PasoConfiguracionPanel({
|
||||
{MATERIAS_MOCK.map((m) => (
|
||||
<div
|
||||
key={m.id}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() =>
|
||||
onChange((w) => ({
|
||||
...w,
|
||||
clonInterno: { ...w.clonInterno, asignaturaOrigenId: m.id },
|
||||
}))
|
||||
}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key !== 'Enter' && e.key !== ' ') return
|
||||
e.preventDefault()
|
||||
onChange((w) => ({
|
||||
...w,
|
||||
clonInterno: { ...w.clonInterno, asignaturaOrigenId: m.id },
|
||||
}))
|
||||
}}
|
||||
className={`hover:bg-accent flex cursor-pointer items-center justify-between rounded-md border p-3 ${
|
||||
wizard.clonInterno?.asignaturaOrigenId === m.id
|
||||
? 'border-primary bg-primary/5 ring-primary ring-1'
|
||||
@@ -245,7 +242,7 @@ export function PasoConfiguracionPanel({
|
||||
)
|
||||
}
|
||||
|
||||
if (wizard.subModoClonado === 'TRADICIONAL') {
|
||||
if (wizard.tipoOrigen === 'CLONADO_TRADICIONAL') {
|
||||
return (
|
||||
<div className="grid gap-4">
|
||||
<div className="rounded-lg border border-dashed p-8 text-center">
|
||||
@@ -1,10 +1,6 @@
|
||||
import * as Icons from 'lucide-react'
|
||||
|
||||
import type {
|
||||
ModoCreacion,
|
||||
NewSubjectWizardState,
|
||||
SubModoClonado,
|
||||
} from '@/features/asignaturas/nueva/types'
|
||||
import type { NewSubjectWizardState } from '@/features/asignaturas/nueva/types'
|
||||
|
||||
import {
|
||||
Card,
|
||||
@@ -21,19 +17,33 @@ export function PasoMetodoCardGroup({
|
||||
wizard: NewSubjectWizardState
|
||||
onChange: React.Dispatch<React.SetStateAction<NewSubjectWizardState>>
|
||||
}) {
|
||||
const isSelected = (m: ModoCreacion) => wizard.modoCreacion === m
|
||||
const isSubSelected = (s: SubModoClonado) => wizard.subModoClonado === s
|
||||
const isSelected = (modo: NewSubjectWizardState['tipoOrigen']) =>
|
||||
wizard.tipoOrigen === modo
|
||||
const handleKeyActivate = (e: React.KeyboardEvent, cb: () => void) => {
|
||||
const key = e.key
|
||||
if (
|
||||
key === 'Enter' ||
|
||||
key === ' ' ||
|
||||
key === 'Spacebar' ||
|
||||
key === 'Space'
|
||||
) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
cb()
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid gap-4 sm:grid-cols-3">
|
||||
<Card
|
||||
className={isSelected('MANUAL') ? 'ring-ring ring-2' : ''}
|
||||
onClick={() =>
|
||||
onChange((w) => ({
|
||||
onChange(
|
||||
(w): NewSubjectWizardState => ({
|
||||
...w,
|
||||
modoCreacion: 'MANUAL',
|
||||
subModoClonado: undefined,
|
||||
}))
|
||||
tipoOrigen: 'MANUAL',
|
||||
}),
|
||||
)
|
||||
}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
@@ -51,11 +61,12 @@ export function PasoMetodoCardGroup({
|
||||
<Card
|
||||
className={isSelected('IA') ? 'ring-ring ring-2' : ''}
|
||||
onClick={() =>
|
||||
onChange((w) => ({
|
||||
onChange(
|
||||
(w): NewSubjectWizardState => ({
|
||||
...w,
|
||||
modoCreacion: 'IA',
|
||||
subModoClonado: undefined,
|
||||
}))
|
||||
tipoOrigen: 'IA',
|
||||
}),
|
||||
)
|
||||
}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
@@ -69,8 +80,10 @@ export function PasoMetodoCardGroup({
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
className={isSelected('CLONADO') ? 'ring-ring ring-2' : ''}
|
||||
onClick={() => onChange((w) => ({ ...w, modoCreacion: 'CLONADO' }))}
|
||||
className={isSelected('OTRO') ? 'ring-ring ring-2' : ''}
|
||||
onClick={() =>
|
||||
onChange((w): NewSubjectWizardState => ({ ...w, tipoOrigen: 'OTRO' }))
|
||||
}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
@@ -80,18 +93,34 @@ export function PasoMetodoCardGroup({
|
||||
</CardTitle>
|
||||
<CardDescription>De otra asignatura o archivo Word.</CardDescription>
|
||||
</CardHeader>
|
||||
{wizard.modoCreacion === 'CLONADO' && (
|
||||
<CardContent>
|
||||
<div className="flex flex-col gap-3">
|
||||
{(wizard.tipoOrigen === 'OTRO' ||
|
||||
wizard.tipoOrigen === 'CLONADO_INTERNO' ||
|
||||
wizard.tipoOrigen === 'CLONADO_TRADICIONAL') && (
|
||||
<CardContent className="flex flex-col gap-3">
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onChange((w) => ({ ...w, subModoClonado: 'INTERNO' }))
|
||||
onChange(
|
||||
(w): NewSubjectWizardState => ({
|
||||
...w,
|
||||
tipoOrigen: 'CLONADO_INTERNO',
|
||||
}),
|
||||
)
|
||||
}}
|
||||
onKeyDown={(e: React.KeyboardEvent) =>
|
||||
handleKeyActivate(e, () =>
|
||||
onChange(
|
||||
(w): NewSubjectWizardState => ({
|
||||
...w,
|
||||
tipoOrigen: 'CLONADO_INTERNO',
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
className={`hover:border-primary/50 hover:bg-accent flex cursor-pointer items-center gap-4 rounded-lg border p-4 text-left transition-all ${
|
||||
isSubSelected('INTERNO')
|
||||
isSelected('CLONADO_INTERNO')
|
||||
? 'bg-primary/5 text-primary ring-primary border-primary ring-1'
|
||||
: 'border-border text-muted-foreground'
|
||||
}`}
|
||||
@@ -110,10 +139,25 @@ export function PasoMetodoCardGroup({
|
||||
tabIndex={0}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onChange((w) => ({ ...w, subModoClonado: 'TRADICIONAL' }))
|
||||
onChange(
|
||||
(w): NewSubjectWizardState => ({
|
||||
...w,
|
||||
tipoOrigen: 'CLONADO_TRADICIONAL',
|
||||
}),
|
||||
)
|
||||
}}
|
||||
onKeyDown={(e: React.KeyboardEvent) =>
|
||||
handleKeyActivate(e, () =>
|
||||
onChange(
|
||||
(w): NewSubjectWizardState => ({
|
||||
...w,
|
||||
tipoOrigen: 'CLONADO_TRADICIONAL',
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
className={`hover:border-primary/50 hover:bg-accent flex cursor-pointer items-center gap-4 rounded-lg border p-4 text-left transition-all ${
|
||||
isSubSelected('TRADICIONAL')
|
||||
isSelected('CLONADO_TRADICIONAL')
|
||||
? 'bg-primary/5 text-primary ring-primary border-primary ring-1'
|
||||
: 'border-border text-muted-foreground'
|
||||
}`}
|
||||
@@ -121,10 +165,7 @@ export function PasoMetodoCardGroup({
|
||||
<Icons.Upload className="h-6 w-6 flex-none" />
|
||||
<div className="flex flex-col">
|
||||
<span className="text-sm font-medium">Desde archivos</span>
|
||||
<span className="text-xs opacity-70">
|
||||
Subir Word existente
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-xs opacity-70">Subir Word existente</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
@@ -9,9 +9,38 @@ import {
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/components/ui/card'
|
||||
import { ESTRUCTURAS_SEP } from '@/features/asignaturas/nueva/catalogs'
|
||||
import { usePlan, useSubjectEstructuras } from '@/data'
|
||||
import { formatFileSize } from '@/features/planes/utils/format-file-size'
|
||||
|
||||
export function PasoResumenCard({ wizard }: { wizard: NewSubjectWizardState }) {
|
||||
const { data: plan } = usePlan(wizard.plan_estudio_id)
|
||||
const { data: estructuras } = useSubjectEstructuras()
|
||||
|
||||
const estructuraNombre = (() => {
|
||||
const estructuraId = wizard.datosBasicos.estructuraId
|
||||
if (!estructuraId) return '—'
|
||||
const hit = estructuras?.find((e) => e.id === estructuraId)
|
||||
return hit?.nombre ?? estructuraId
|
||||
})()
|
||||
|
||||
const modoLabel = (() => {
|
||||
if (wizard.tipoOrigen === 'MANUAL') return 'Manual (Vacía)'
|
||||
if (wizard.tipoOrigen === 'IA') return 'Generada con IA'
|
||||
if (wizard.tipoOrigen === 'CLONADO_INTERNO') return 'Clonada (Sistema)'
|
||||
if (wizard.tipoOrigen === 'CLONADO_TRADICIONAL') return 'Clonada (Archivo)'
|
||||
return '—'
|
||||
})()
|
||||
|
||||
const creditosText =
|
||||
typeof wizard.datosBasicos.creditos === 'number' &&
|
||||
Number.isFinite(wizard.datosBasicos.creditos)
|
||||
? wizard.datosBasicos.creditos.toFixed(2)
|
||||
: '—'
|
||||
|
||||
const archivosRef = wizard.iaConfig?.archivosReferencia ?? []
|
||||
const repositoriosRef = wizard.iaConfig?.repositoriosReferencia ?? []
|
||||
const adjuntos = wizard.iaConfig?.archivosAdjuntos ?? []
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
@@ -20,53 +49,145 @@ export function PasoResumenCard({ wizard }: { wizard: NewSubjectWizardState }) {
|
||||
Verifica los datos antes de crear la asignatura.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="grid gap-4 text-sm">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<CardContent>
|
||||
<div className="grid gap-4 text-sm">
|
||||
<div className="grid gap-2">
|
||||
<div>
|
||||
<span className="text-muted-foreground">Nombre:</span>
|
||||
<div className="font-medium">{wizard.datosBasicos.nombre}</div>
|
||||
<span className="text-muted-foreground">Plan de estudios: </span>
|
||||
<span className="font-medium">
|
||||
{plan?.nombre || wizard.plan_estudio_id || '—'}
|
||||
</span>
|
||||
</div>
|
||||
{plan?.carreras?.nombre ? (
|
||||
<div>
|
||||
<span className="text-muted-foreground">Tipo:</span>
|
||||
<div className="font-medium">{wizard.datosBasicos.tipo}</div>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-muted-foreground">Créditos:</span>
|
||||
<div className="font-medium">{wizard.datosBasicos.creditos}</div>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-muted-foreground">Estructura:</span>
|
||||
<div className="font-medium">
|
||||
{
|
||||
ESTRUCTURAS_SEP.find(
|
||||
(e) => e.id === wizard.datosBasicos.estructuraId,
|
||||
)?.label
|
||||
}
|
||||
</div>
|
||||
<span className="text-muted-foreground">Carrera: </span>
|
||||
<span className="font-medium">{plan.carreras.nombre}</span>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="bg-muted rounded-md p-3">
|
||||
<span className="text-muted-foreground">Modo de creación:</span>
|
||||
<div className="flex items-center gap-2 font-medium">
|
||||
{wizard.modoCreacion === 'MANUAL' && (
|
||||
<>
|
||||
<Icons.Pencil className="h-4 w-4" /> Manual (Vacía)
|
||||
</>
|
||||
<span className="text-muted-foreground">Tipo de origen: </span>
|
||||
<span className="inline-flex items-center gap-2 font-medium">
|
||||
{wizard.tipoOrigen === 'MANUAL' && (
|
||||
<Icons.Pencil className="h-4 w-4" />
|
||||
)}
|
||||
{wizard.modoCreacion === 'IA' && (
|
||||
<>
|
||||
<Icons.Sparkles className="h-4 w-4" /> Generada con IA
|
||||
</>
|
||||
{wizard.tipoOrigen === 'IA' && (
|
||||
<Icons.Sparkles className="h-4 w-4" />
|
||||
)}
|
||||
{wizard.modoCreacion === 'CLONADO' && (
|
||||
<>
|
||||
<Icons.Copy className="h-4 w-4" /> Clonada
|
||||
{wizard.subModoClonado === 'INTERNO'
|
||||
? ' (Sistema)'
|
||||
: ' (Archivo)'}
|
||||
</>
|
||||
{(wizard.tipoOrigen === 'CLONADO_INTERNO' ||
|
||||
wizard.tipoOrigen === 'CLONADO_TRADICIONAL') && (
|
||||
<Icons.Copy className="h-4 w-4" />
|
||||
)}
|
||||
{modoLabel}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="col-span-2">
|
||||
<span className="text-muted-foreground">Nombre: </span>
|
||||
<span className="font-medium">
|
||||
{wizard.datosBasicos.nombre || '—'}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-muted-foreground">Código: </span>
|
||||
<span className="font-medium">
|
||||
{wizard.datosBasicos.codigo || '—'}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-muted-foreground">Tipo: </span>
|
||||
<span className="font-medium">
|
||||
{wizard.datosBasicos.tipo || '—'}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-muted-foreground">Créditos: </span>
|
||||
<span className="font-medium">{creditosText}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-muted-foreground">Estructura: </span>
|
||||
<span className="font-medium">{estructuraNombre}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-muted-foreground">Horas académicas: </span>
|
||||
<span className="font-medium">
|
||||
{wizard.datosBasicos.horasAcademicas ?? '—'}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-muted-foreground">
|
||||
Horas independientes:{' '}
|
||||
</span>
|
||||
<span className="font-medium">
|
||||
{wizard.datosBasicos.horasIndependientes ?? '—'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-muted/50 rounded-md p-3">
|
||||
<div className="font-medium">Configuración IA</div>
|
||||
<div className="mt-2 grid gap-2">
|
||||
<div>
|
||||
<span className="text-muted-foreground">
|
||||
Enfoque académico:{' '}
|
||||
</span>
|
||||
<span className="font-medium">
|
||||
{wizard.iaConfig?.descripcionEnfoqueAcademico || '—'}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-muted-foreground">
|
||||
Instrucciones adicionales:{' '}
|
||||
</span>
|
||||
<span className="font-medium">
|
||||
{wizard.iaConfig?.instruccionesAdicionalesIA || '—'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="mt-2">
|
||||
<div className="font-medium">Archivos de referencia</div>
|
||||
{archivosRef.length ? (
|
||||
<ul className="text-muted-foreground list-disc pl-5 text-xs">
|
||||
{archivosRef.map((id) => (
|
||||
<li key={id}>{id}</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<div className="text-muted-foreground text-xs">—</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="font-medium">Repositorios de referencia</div>
|
||||
{repositoriosRef.length ? (
|
||||
<ul className="text-muted-foreground list-disc pl-5 text-xs">
|
||||
{repositoriosRef.map((id) => (
|
||||
<li key={id}>{id}</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<div className="text-muted-foreground text-xs">—</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="font-medium">Archivos adjuntos</div>
|
||||
{adjuntos.length ? (
|
||||
<ul className="text-muted-foreground list-disc pl-5 text-xs">
|
||||
{adjuntos.map((f) => (
|
||||
<li key={f.id}>
|
||||
<span className="text-foreground">{f.file.name}</span>{' '}
|
||||
<span>· {formatFileSize(f.file.size)}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<div className="text-muted-foreground text-xs">—</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
@@ -3,62 +3,71 @@ import type { NewSubjectWizardState } from '@/features/asignaturas/nueva/types'
|
||||
import { Button } from '@/components/ui/button'
|
||||
|
||||
export function WizardControls({
|
||||
Wizard,
|
||||
methods,
|
||||
wizard,
|
||||
canContinueDesdeMetodo,
|
||||
canContinueDesdeBasicos,
|
||||
canContinueDesdeConfig,
|
||||
setWizard,
|
||||
errorMessage,
|
||||
onPrev,
|
||||
onNext,
|
||||
disablePrev,
|
||||
disableNext,
|
||||
disableCreate,
|
||||
isLastStep,
|
||||
onCreate,
|
||||
}: {
|
||||
Wizard: any
|
||||
methods: any
|
||||
wizard: NewSubjectWizardState
|
||||
canContinueDesdeMetodo: boolean
|
||||
canContinueDesdeBasicos: boolean
|
||||
canContinueDesdeConfig: boolean
|
||||
onCreate: () => void
|
||||
setWizard: React.Dispatch<React.SetStateAction<NewSubjectWizardState>>
|
||||
errorMessage?: string | null
|
||||
onPrev: () => void
|
||||
onNext: () => void
|
||||
disablePrev: boolean
|
||||
disableNext: boolean
|
||||
disableCreate: boolean
|
||||
isLastStep: boolean
|
||||
onCreate: () => Promise<void> | void
|
||||
}) {
|
||||
const idx = Wizard.utils.getIndex(methods.current.id)
|
||||
const isLast = idx >= Wizard.steps.length - 1
|
||||
const handleCreate = async () => {
|
||||
setWizard((w) => ({
|
||||
...w,
|
||||
isLoading: true,
|
||||
errorMessage: null,
|
||||
}))
|
||||
|
||||
try {
|
||||
await onCreate()
|
||||
} catch (err: any) {
|
||||
setWizard((w) => ({
|
||||
...w,
|
||||
isLoading: false,
|
||||
errorMessage: err?.message ?? 'Error creando la asignatura',
|
||||
}))
|
||||
} finally {
|
||||
setWizard((w) => ({ ...w, isLoading: false }))
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex grow items-center justify-between">
|
||||
<Button variant="secondary" onClick={onPrev} disabled={disablePrev}>
|
||||
Anterior
|
||||
</Button>
|
||||
|
||||
<div className="flex-1">
|
||||
{wizard.errorMessage && (
|
||||
{(errorMessage ?? wizard.errorMessage) && (
|
||||
<span className="text-destructive text-sm font-medium">
|
||||
{wizard.errorMessage}
|
||||
{errorMessage ?? wizard.errorMessage}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4">
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => methods.prev()}
|
||||
disabled={idx === 0 || wizard.isLoading}
|
||||
>
|
||||
Anterior
|
||||
</Button>
|
||||
|
||||
{!isLast ? (
|
||||
<Button
|
||||
onClick={() => methods.next()}
|
||||
disabled={
|
||||
wizard.isLoading ||
|
||||
(idx === 0 && !canContinueDesdeMetodo) ||
|
||||
(idx === 1 && !canContinueDesdeBasicos) ||
|
||||
(idx === 2 && !canContinueDesdeConfig)
|
||||
}
|
||||
>
|
||||
Siguiente
|
||||
{isLastStep ? (
|
||||
<Button onClick={handleCreate} disabled={disableCreate}>
|
||||
{wizard.isLoading ? 'Creando...' : 'Crear Asignatura'}
|
||||
</Button>
|
||||
) : (
|
||||
<Button onClick={onCreate} disabled={wizard.isLoading}>
|
||||
{wizard.isLoading ? 'Creando...' : 'Crear Asignatura'}
|
||||
<Button onClick={onNext} disabled={disableNext}>
|
||||
Siguiente
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -246,9 +246,9 @@ export function PasoBasicosForm({
|
||||
// Keep undefined when the input is empty so the field stays optional
|
||||
numCiclos: (() => {
|
||||
const raw = e.target.value
|
||||
if (raw === '') return undefined
|
||||
if (raw === '') return null
|
||||
const asNumber = Number(raw)
|
||||
if (Number.isNaN(asNumber)) return undefined
|
||||
if (Number.isNaN(asNumber)) return null
|
||||
// Coerce to positive integer (natural numbers without zero)
|
||||
const n = Math.floor(Math.abs(asNumber))
|
||||
return n >= 1 ? n : 1
|
||||
|
||||
@@ -90,7 +90,7 @@ export function PasoDetallesPanel({
|
||||
selectedRepositorioIds={wizard.iaConfig?.repositoriosReferencia || []}
|
||||
uploadedFiles={wizard.iaConfig?.archivosAdjuntos || []}
|
||||
onToggleArchivo={(id, checked) =>
|
||||
onChange((w) => {
|
||||
onChange((w): NewPlanWizardState => {
|
||||
const prev = w.iaConfig?.archivosReferencia || []
|
||||
const next = checked
|
||||
? [...prev, id]
|
||||
@@ -105,7 +105,7 @@ export function PasoDetallesPanel({
|
||||
})
|
||||
}
|
||||
onToggleRepositorio={(id, checked) =>
|
||||
onChange((w) => {
|
||||
onChange((w): NewPlanWizardState => {
|
||||
const prev = w.iaConfig?.repositoriosReferencia || []
|
||||
const next = checked
|
||||
? [...prev, id]
|
||||
|
||||
@@ -77,11 +77,11 @@ export function WizardControls({
|
||||
|
||||
console.log(`${new Date().toISOString()} - Enviando a generar plan IA`)
|
||||
|
||||
const data = await generatePlanAI.mutateAsync(aiInput as any)
|
||||
console.log(`${new Date().toISOString()} - Plan IA generado`, data)
|
||||
const plan = await generatePlanAI.mutateAsync(aiInput as any)
|
||||
console.log(`${new Date().toISOString()} - Plan IA generado`, plan)
|
||||
|
||||
navigate({
|
||||
to: `/planes/${data.plan.id}`,
|
||||
to: `/planes/${plan.id}`,
|
||||
state: { showConfetti: true },
|
||||
})
|
||||
return
|
||||
@@ -122,7 +122,7 @@ export function WizardControls({
|
||||
<Button variant="secondary" onClick={onPrev} disabled={disablePrev}>
|
||||
Anterior
|
||||
</Button>
|
||||
<div className="flex-1">
|
||||
<div className="mx-2 flex-1">
|
||||
{errorMessage && (
|
||||
<span className="text-destructive text-sm font-medium">
|
||||
{errorMessage}
|
||||
|
||||
@@ -268,8 +268,8 @@ export type AIGeneratePlanInput = {
|
||||
estructuraPlanId: UUID
|
||||
}
|
||||
iaConfig: {
|
||||
descripcionEnfoque: string
|
||||
notasAdicionales?: string
|
||||
descripcionEnfoqueAcademico: string
|
||||
instruccionesAdicionalesIA?: string
|
||||
archivosReferencia?: Array<UUID>
|
||||
repositoriosIds?: Array<UUID>
|
||||
archivosAdjuntos: Array<UploadedFile>
|
||||
|
||||
@@ -11,6 +11,7 @@ import type {
|
||||
TipoAsignatura,
|
||||
UUID,
|
||||
} from '../types/domain'
|
||||
import type { Database } from '@/types/supabase'
|
||||
|
||||
const EDGE = {
|
||||
subjects_create_manual: 'subjects_create_manual',
|
||||
@@ -226,3 +227,20 @@ export async function subjects_get_document(
|
||||
subjectId,
|
||||
})
|
||||
}
|
||||
|
||||
export async function subjects_get_structure_catalog(): Promise<
|
||||
Array<Database['public']['Tables']['estructuras_asignatura']['Row']>
|
||||
> {
|
||||
const supabase = supabaseBrowser()
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('estructuras_asignatura')
|
||||
.select('*')
|
||||
.order('nombre', { ascending: true })
|
||||
|
||||
if (error) {
|
||||
throw error
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
@@ -98,7 +98,7 @@ export function usePlanDocumento(planId: UUID | null | undefined) {
|
||||
|
||||
export function useCatalogosPlanes() {
|
||||
return useQuery({
|
||||
queryKey: ['catalogos_planes'],
|
||||
queryKey: qk.estructurasPlan(),
|
||||
queryFn: getCatalogos,
|
||||
staleTime: 1000 * 60 * 60, // 1 hora de caché (estos datos casi no cambian)
|
||||
})
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
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,
|
||||
subjects_bibliografia_list,
|
||||
@@ -14,6 +8,7 @@ import {
|
||||
subjects_generate_document,
|
||||
subjects_get,
|
||||
subjects_get_document,
|
||||
subjects_get_structure_catalog,
|
||||
subjects_history,
|
||||
subjects_import_from_file,
|
||||
subjects_persist_from_ai,
|
||||
@@ -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({
|
||||
@@ -63,6 +66,13 @@ export function useSubjectDocumento(subjectId: UUID | null | undefined) {
|
||||
})
|
||||
}
|
||||
|
||||
export function useSubjectEstructuras() {
|
||||
return useQuery({
|
||||
queryKey: qk.estructurasAsignatura(),
|
||||
queryFn: () => subjects_get_structure_catalog(),
|
||||
})
|
||||
}
|
||||
|
||||
/* ------------------ Mutations ------------------ */
|
||||
|
||||
export function useCreateSubjectManual() {
|
||||
@@ -159,7 +169,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)
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
import { supabaseBrowser } from "./client";
|
||||
import {
|
||||
FunctionsFetchError,
|
||||
FunctionsHttpError,
|
||||
FunctionsRelayError,
|
||||
} from '@supabase/supabase-js'
|
||||
|
||||
import type { Database } from "@/types/supabase";
|
||||
import type { SupabaseClient } from "@supabase/supabase-js";
|
||||
import { supabaseBrowser } from './client'
|
||||
|
||||
import type { Database } from '@/types/supabase'
|
||||
import type { SupabaseClient } from '@supabase/supabase-js'
|
||||
|
||||
export type EdgeInvokeOptions = {
|
||||
method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
||||
headers?: Record<string, string>;
|
||||
};
|
||||
method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'
|
||||
headers?: Record<string, string>
|
||||
}
|
||||
|
||||
export class EdgeFunctionError extends Error {
|
||||
constructor(
|
||||
@@ -15,8 +21,8 @@ export class EdgeFunctionError extends Error {
|
||||
public readonly status?: number,
|
||||
public readonly details?: unknown,
|
||||
) {
|
||||
super(message);
|
||||
this.name = "EdgeFunctionError";
|
||||
super(message)
|
||||
this.name = 'EdgeFunctionError'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,23 +40,69 @@ export async function invokeEdge<TOut>(
|
||||
opts: EdgeInvokeOptions = {},
|
||||
client?: SupabaseClient<Database>,
|
||||
): Promise<TOut> {
|
||||
const supabase = client ?? supabaseBrowser();
|
||||
const supabase = client ?? supabaseBrowser()
|
||||
|
||||
const { data, error } = await supabase.functions.invoke(functionName, {
|
||||
body,
|
||||
method: opts.method ?? "POST",
|
||||
method: opts.method ?? 'POST',
|
||||
headers: opts.headers,
|
||||
});
|
||||
})
|
||||
|
||||
if (error) {
|
||||
const anyErr = error;
|
||||
throw new EdgeFunctionError(
|
||||
anyErr.message ?? "Error en Edge Function",
|
||||
functionName,
|
||||
anyErr.status,
|
||||
anyErr,
|
||||
);
|
||||
// Valores por defecto (por si falla el parseo o es otro tipo de error)
|
||||
let message = error.message // El genérico "returned a non-2xx status code"
|
||||
let status = undefined
|
||||
let details: unknown = error
|
||||
|
||||
// 2. Verificamos si es un error HTTP (4xx o 5xx) que trae cuerpo JSON
|
||||
if (error instanceof FunctionsHttpError) {
|
||||
try {
|
||||
// Obtenemos el status real (ej. 404, 400)
|
||||
status = error.context.status
|
||||
|
||||
// ¡LA CLAVE! Leemos el JSON que tu Edge Function envió
|
||||
const errorBody = await error.context.json()
|
||||
details = errorBody
|
||||
|
||||
// Intentamos extraer el mensaje humano según tu estructura { error: { message: "..." } }
|
||||
// o la estructura simple { error: "..." }
|
||||
if (errorBody && typeof errorBody === 'object') {
|
||||
// Caso 1: Estructura anidada (la que definimos hace poco: { error: { message: "..." } })
|
||||
if (
|
||||
'error' in errorBody &&
|
||||
typeof errorBody.error === 'object' &&
|
||||
errorBody.error !== null &&
|
||||
'message' in errorBody.error
|
||||
) {
|
||||
message = (errorBody.error as { message: string }).message
|
||||
}
|
||||
// Caso 2: Estructura simple ({ error: "Mensaje de error" })
|
||||
else if (
|
||||
'error' in errorBody &&
|
||||
typeof errorBody.error === 'string'
|
||||
) {
|
||||
message = errorBody.error
|
||||
}
|
||||
// Caso 3: Propiedad message directa ({ message: "..." })
|
||||
else if (
|
||||
'message' in errorBody &&
|
||||
typeof errorBody.message === 'string'
|
||||
) {
|
||||
message = errorBody.message
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('No se pudo parsear el error JSON de la Edge Function', e)
|
||||
}
|
||||
} else if (error instanceof FunctionsRelayError) {
|
||||
message = `Error de Relay Supabase: ${error.message}`
|
||||
} else if (error instanceof FunctionsFetchError) {
|
||||
message = `Error de conexión (Fetch): ${error.message}`
|
||||
}
|
||||
|
||||
return data as TOut;
|
||||
// 3. Lanzamos tu error personalizado con los datos reales extraídos
|
||||
throw new EdgeFunctionError(message, functionName, status, details)
|
||||
}
|
||||
|
||||
return data as TOut
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import * as Icons from 'lucide-react'
|
||||
import { useNuevaAsignaturaWizard } from './hooks/useNuevaAsignaturaWizard'
|
||||
|
||||
import { PasoBasicosForm } from '@/components/asignaturas/wizard/PasoBasicosForm'
|
||||
import { PasoConfiguracionPanel } from '@/components/asignaturas/wizard/PasoConfiguracionPanel'
|
||||
import { PasoDetallesPanel } from '@/components/asignaturas/wizard/PasoDetallesPanel'
|
||||
import { PasoMetodoCardGroup } from '@/components/asignaturas/wizard/PasoMetodoCardGroup'
|
||||
import { PasoResumenCard } from '@/components/asignaturas/wizard/PasoResumenCard'
|
||||
import { WizardControls } from '@/components/asignaturas/wizard/WizardControls'
|
||||
@@ -54,7 +54,7 @@ export function NuevaAsignaturaModalContainer({ planId }: { planId: string }) {
|
||||
setWizard,
|
||||
canContinueDesdeMetodo,
|
||||
canContinueDesdeBasicos,
|
||||
canContinueDesdeConfig,
|
||||
canContinueDesdeDetalles,
|
||||
simularGeneracionIA,
|
||||
crearAsignatura,
|
||||
} = useNuevaAsignaturaWizard(planId)
|
||||
@@ -104,13 +104,24 @@ export function NuevaAsignaturaModalContainer({ planId }: { planId: string }) {
|
||||
footerSlot={
|
||||
<Wizard.Stepper.Controls>
|
||||
<WizardControls
|
||||
Wizard={Wizard}
|
||||
methods={methods}
|
||||
errorMessage={wizard.errorMessage}
|
||||
onPrev={() => methods.prev()}
|
||||
onNext={() => methods.next()}
|
||||
disablePrev={idx === 0 || wizard.isLoading}
|
||||
disableNext={
|
||||
wizard.isLoading ||
|
||||
(idx === 0 && !canContinueDesdeMetodo) ||
|
||||
(idx === 1 && !canContinueDesdeBasicos) ||
|
||||
(idx === 2 && !canContinueDesdeDetalles)
|
||||
}
|
||||
disableCreate={wizard.isLoading}
|
||||
isLastStep={idx >= Wizard.steps.length - 1}
|
||||
wizard={wizard}
|
||||
canContinueDesdeMetodo={canContinueDesdeMetodo}
|
||||
canContinueDesdeBasicos={canContinueDesdeBasicos}
|
||||
canContinueDesdeConfig={canContinueDesdeConfig}
|
||||
onCreate={() => crearAsignatura(handleClose)}
|
||||
setWizard={setWizard}
|
||||
onCreate={async () => {
|
||||
await crearAsignatura()
|
||||
handleClose()
|
||||
}}
|
||||
/>
|
||||
</Wizard.Stepper.Controls>
|
||||
}
|
||||
@@ -130,7 +141,7 @@ export function NuevaAsignaturaModalContainer({ planId }: { planId: string }) {
|
||||
|
||||
{idx === 2 && (
|
||||
<Wizard.Stepper.Panel>
|
||||
<PasoConfiguracionPanel
|
||||
<PasoDetallesPanel
|
||||
wizard={wizard}
|
||||
onChange={setWizard}
|
||||
onGenerarIA={simularGeneracionIA}
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
import { useState } from "react";
|
||||
import { useState } from 'react'
|
||||
|
||||
import type { AsignaturaPreview, NewSubjectWizardState } from "../types";
|
||||
import type { AsignaturaPreview, NewSubjectWizardState } from '../types'
|
||||
|
||||
export function useNuevaAsignaturaWizard(planId: string) {
|
||||
const [wizard, setWizard] = useState<NewSubjectWizardState>({
|
||||
step: 1,
|
||||
planId,
|
||||
modoCreacion: null,
|
||||
plan_estudio_id: planId,
|
||||
tipoOrigen: null,
|
||||
datosBasicos: {
|
||||
nombre: "",
|
||||
clave: "",
|
||||
tipo: "OBLIGATORIA",
|
||||
creditos: 0,
|
||||
horasSemana: 0,
|
||||
estructuraId: "",
|
||||
nombre: '',
|
||||
codigo: '',
|
||||
tipo: null,
|
||||
creditos: null,
|
||||
horasAcademicas: null,
|
||||
horasIndependientes: null,
|
||||
estructuraId: '',
|
||||
},
|
||||
clonInterno: {},
|
||||
clonTradicional: {
|
||||
@@ -21,42 +22,47 @@ export function useNuevaAsignaturaWizard(planId: string) {
|
||||
archivosAdicionalesIds: [],
|
||||
},
|
||||
iaConfig: {
|
||||
descripcionEnfoque: "",
|
||||
notasAdicionales: "",
|
||||
archivosExistentesIds: [],
|
||||
descripcionEnfoqueAcademico: '',
|
||||
instruccionesAdicionalesIA: '',
|
||||
archivosReferencia: [],
|
||||
repositoriosReferencia: [],
|
||||
archivosAdjuntos: [],
|
||||
},
|
||||
resumen: {},
|
||||
isLoading: false,
|
||||
errorMessage: null,
|
||||
});
|
||||
})
|
||||
|
||||
const canContinueDesdeMetodo = wizard.modoCreacion === "MANUAL" ||
|
||||
wizard.modoCreacion === "IA" ||
|
||||
(wizard.modoCreacion === "CLONADO" && !!wizard.subModoClonado);
|
||||
const canContinueDesdeMetodo =
|
||||
wizard.tipoOrigen === 'MANUAL' ||
|
||||
wizard.tipoOrigen === 'IA' ||
|
||||
wizard.tipoOrigen === 'CLONADO_INTERNO' ||
|
||||
wizard.tipoOrigen === 'CLONADO_TRADICIONAL'
|
||||
|
||||
const canContinueDesdeBasicos = !!wizard.datosBasicos.nombre &&
|
||||
const canContinueDesdeBasicos =
|
||||
!!wizard.datosBasicos.nombre &&
|
||||
wizard.datosBasicos.tipo !== null &&
|
||||
wizard.datosBasicos.creditos !== null &&
|
||||
wizard.datosBasicos.creditos > 0 &&
|
||||
!!wizard.datosBasicos.estructuraId;
|
||||
!!wizard.datosBasicos.estructuraId
|
||||
|
||||
const canContinueDesdeConfig = (() => {
|
||||
if (wizard.modoCreacion === "MANUAL") return true;
|
||||
if (wizard.modoCreacion === "IA") {
|
||||
return !!wizard.iaConfig?.descripcionEnfoque;
|
||||
const canContinueDesdeDetalles = (() => {
|
||||
if (wizard.tipoOrigen === 'MANUAL') return true
|
||||
if (wizard.tipoOrigen === 'IA') {
|
||||
return !!wizard.iaConfig?.descripcionEnfoqueAcademico
|
||||
}
|
||||
if (wizard.modoCreacion === "CLONADO") {
|
||||
if (wizard.subModoClonado === "INTERNO") {
|
||||
return !!wizard.clonInterno?.asignaturaOrigenId;
|
||||
if (wizard.tipoOrigen === 'CLONADO_INTERNO') {
|
||||
return !!wizard.clonInterno?.asignaturaOrigenId
|
||||
}
|
||||
if (wizard.subModoClonado === "TRADICIONAL") {
|
||||
return !!wizard.clonTradicional?.archivoWordAsignaturaId;
|
||||
if (wizard.tipoOrigen === 'CLONADO_TRADICIONAL') {
|
||||
return !!wizard.clonTradicional?.archivoWordAsignaturaId
|
||||
}
|
||||
}
|
||||
return false;
|
||||
})();
|
||||
return false
|
||||
})()
|
||||
|
||||
const simularGeneracionIA = async () => {
|
||||
setWizard((w) => ({ ...w, isLoading: true }));
|
||||
await new Promise((r) => setTimeout(r, 1500));
|
||||
setWizard((w) => ({ ...w, isLoading: true }))
|
||||
await new Promise((r) => setTimeout(r, 1500))
|
||||
setWizard((w) => ({
|
||||
...w,
|
||||
isLoading: false,
|
||||
@@ -64,27 +70,25 @@ export function useNuevaAsignaturaWizard(planId: string) {
|
||||
previewAsignatura: {
|
||||
nombre: w.datosBasicos.nombre,
|
||||
objetivo:
|
||||
"Aplicar los fundamentos teóricos para la resolución de problemas...",
|
||||
'Aplicar los fundamentos teóricos para la resolución de problemas...',
|
||||
unidades: 5,
|
||||
bibliografiaCount: 3,
|
||||
} as AsignaturaPreview,
|
||||
},
|
||||
}));
|
||||
};
|
||||
}))
|
||||
}
|
||||
|
||||
const crearAsignatura = async (onCreated: () => void) => {
|
||||
setWizard((w) => ({ ...w, isLoading: true }));
|
||||
await new Promise((r) => setTimeout(r, 1000));
|
||||
onCreated();
|
||||
};
|
||||
const crearAsignatura = async () => {
|
||||
await new Promise((r) => setTimeout(r, 1000))
|
||||
}
|
||||
|
||||
return {
|
||||
wizard,
|
||||
setWizard,
|
||||
canContinueDesdeMetodo,
|
||||
canContinueDesdeBasicos,
|
||||
canContinueDesdeConfig,
|
||||
canContinueDesdeDetalles,
|
||||
simularGeneracionIA,
|
||||
crearAsignatura,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,45 +1,50 @@
|
||||
export type ModoCreacion = "MANUAL" | "IA" | "CLONADO";
|
||||
export type SubModoClonado = "INTERNO" | "TRADICIONAL";
|
||||
export type TipoAsignatura = "OBLIGATORIA" | "OPTATIVA" | "TRONCAL" | "OTRO";
|
||||
import type { UploadedFile } from '@/components/planes/wizard/PasoDetallesPanel/FileDropZone'
|
||||
import type { Asignatura } from '@/data'
|
||||
|
||||
export type ModoCreacion = 'MANUAL' | 'IA' | 'CLONADO'
|
||||
export type SubModoClonado = 'INTERNO' | 'TRADICIONAL'
|
||||
export type TipoAsignatura = 'OBLIGATORIA' | 'OPTATIVA' | 'TRONCAL' | 'OTRO'
|
||||
|
||||
export type AsignaturaPreview = {
|
||||
nombre: string;
|
||||
objetivo: string;
|
||||
unidades: number;
|
||||
bibliografiaCount: number;
|
||||
};
|
||||
nombre: string
|
||||
objetivo: string
|
||||
unidades: number
|
||||
bibliografiaCount: number
|
||||
}
|
||||
|
||||
export type NewSubjectWizardState = {
|
||||
step: 1 | 2 | 3 | 4;
|
||||
planId: string;
|
||||
modoCreacion: ModoCreacion | null;
|
||||
subModoClonado?: SubModoClonado;
|
||||
step: 1 | 2 | 3 | 4
|
||||
plan_estudio_id: Asignatura['plan_estudio_id']
|
||||
tipoOrigen: Asignatura['tipo_origen'] | null
|
||||
datosBasicos: {
|
||||
nombre: string;
|
||||
clave?: string;
|
||||
tipo: TipoAsignatura;
|
||||
creditos: number;
|
||||
horasSemana?: number;
|
||||
estructuraId: string;
|
||||
};
|
||||
nombre: Asignatura['nombre']
|
||||
codigo?: Asignatura['codigo']
|
||||
tipo: Asignatura['tipo'] | null
|
||||
creditos: Asignatura['creditos'] | null
|
||||
horasAcademicas?: Asignatura['horas_academicas'] | null
|
||||
horasIndependientes?: Asignatura['horas_independientes'] | null
|
||||
estructuraId: Asignatura['estructura_id'] | null
|
||||
}
|
||||
clonInterno?: {
|
||||
facultadId?: string;
|
||||
carreraId?: string;
|
||||
planOrigenId?: string;
|
||||
asignaturaOrigenId?: string | null;
|
||||
};
|
||||
facultadId?: string
|
||||
carreraId?: string
|
||||
planOrigenId?: string
|
||||
asignaturaOrigenId?: string | null
|
||||
}
|
||||
clonTradicional?: {
|
||||
archivoWordAsignaturaId: string | null;
|
||||
archivosAdicionalesIds: Array<string>;
|
||||
};
|
||||
archivoWordAsignaturaId: string | null
|
||||
archivosAdicionalesIds: Array<string>
|
||||
}
|
||||
iaConfig?: {
|
||||
descripcionEnfoque: string;
|
||||
notasAdicionales: string;
|
||||
archivosExistentesIds: Array<string>;
|
||||
};
|
||||
descripcionEnfoqueAcademico: string
|
||||
instruccionesAdicionalesIA: string
|
||||
archivosReferencia: Array<string>
|
||||
repositoriosReferencia?: Array<string>
|
||||
archivosAdjuntos?: Array<UploadedFile>
|
||||
}
|
||||
resumen: {
|
||||
previewAsignatura?: AsignaturaPreview;
|
||||
};
|
||||
isLoading: boolean;
|
||||
errorMessage: string | null;
|
||||
};
|
||||
previewAsignatura?: AsignaturaPreview
|
||||
}
|
||||
isLoading: boolean
|
||||
errorMessage: string | null
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ export function useNuevoPlanWizard() {
|
||||
carrera: { id: '', nombre: '' },
|
||||
nivel: '',
|
||||
tipoCiclo: '',
|
||||
numCiclos: undefined,
|
||||
numCiclos: null,
|
||||
estructuraPlanId: null,
|
||||
},
|
||||
// datosBasicos: {
|
||||
@@ -56,7 +56,7 @@ export function useNuevoPlanWizard() {
|
||||
!!wizard.datosBasicos.carrera.id &&
|
||||
!!wizard.datosBasicos.facultad.id &&
|
||||
!!wizard.datosBasicos.nivel &&
|
||||
wizard.datosBasicos.numCiclos !== undefined &&
|
||||
wizard.datosBasicos.numCiclos !== null &&
|
||||
wizard.datosBasicos.numCiclos > 0 &&
|
||||
// Requerir ambas plantillas (plan y mapa) con versión
|
||||
!!wizard.datosBasicos.estructuraPlanId
|
||||
|
||||
@@ -29,7 +29,7 @@ export type NewPlanWizardState = {
|
||||
}
|
||||
nivel: NivelPlanEstudio | ''
|
||||
tipoCiclo: TipoCiclo | ''
|
||||
numCiclos: number | undefined
|
||||
numCiclos: number | null
|
||||
// Selección de plantillas (obligatorias)
|
||||
estructuraPlanId: string | null
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ import { Route as PlanesPlanIdDetalleIaplanRouteImport } from './routes/planes/$
|
||||
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 PlanesPlanIdDetalleAsignaturasIndexRouteImport } from './routes/planes/$planId/_detalle/asignaturas/index'
|
||||
import { Route as PlanesPlanIdDetalleAsignaturasRouteImport } from './routes/planes/$planId/_detalle/asignaturas'
|
||||
import { Route as PlanesPlanIdDetalleAsignaturasNuevaRouteImport } from './routes/planes/$planId/_detalle/asignaturas/nueva'
|
||||
|
||||
const LoginRoute = LoginRouteImport.update({
|
||||
@@ -102,17 +102,17 @@ const PlanesPlanIdDetalleDocumentoRoute =
|
||||
path: '/documento',
|
||||
getParentRoute: () => PlanesPlanIdDetalleRoute,
|
||||
} as any)
|
||||
const PlanesPlanIdDetalleAsignaturasIndexRoute =
|
||||
PlanesPlanIdDetalleAsignaturasIndexRouteImport.update({
|
||||
id: '/asignaturas/',
|
||||
path: '/asignaturas/',
|
||||
const PlanesPlanIdDetalleAsignaturasRoute =
|
||||
PlanesPlanIdDetalleAsignaturasRouteImport.update({
|
||||
id: '/asignaturas',
|
||||
path: '/asignaturas',
|
||||
getParentRoute: () => PlanesPlanIdDetalleRoute,
|
||||
} as any)
|
||||
const PlanesPlanIdDetalleAsignaturasNuevaRoute =
|
||||
PlanesPlanIdDetalleAsignaturasNuevaRouteImport.update({
|
||||
id: '/asignaturas/nueva',
|
||||
path: '/asignaturas/nueva',
|
||||
getParentRoute: () => PlanesPlanIdDetalleRoute,
|
||||
id: '/nueva',
|
||||
path: '/nueva',
|
||||
getParentRoute: () => PlanesPlanIdDetalleAsignaturasRoute,
|
||||
} as any)
|
||||
|
||||
export interface FileRoutesByFullPath {
|
||||
@@ -123,6 +123,7 @@ export interface FileRoutesByFullPath {
|
||||
'/planes': typeof PlanesListaRouteWithChildren
|
||||
'/planes/$planId': typeof PlanesPlanIdDetalleRouteWithChildren
|
||||
'/planes/nuevo': typeof PlanesListaNuevoRoute
|
||||
'/planes/$planId/asignaturas': typeof PlanesPlanIdDetalleAsignaturasRouteWithChildren
|
||||
'/planes/$planId/documento': typeof PlanesPlanIdDetalleDocumentoRoute
|
||||
'/planes/$planId/flujo': typeof PlanesPlanIdDetalleFlujoRoute
|
||||
'/planes/$planId/historial': typeof PlanesPlanIdDetalleHistorialRoute
|
||||
@@ -131,7 +132,6 @@ export interface FileRoutesByFullPath {
|
||||
'/planes/$planId/asignaturas/$asignaturaId': typeof PlanesPlanIdAsignaturasAsignaturaIdRoute
|
||||
'/planes/$planId/': typeof PlanesPlanIdDetalleIndexRoute
|
||||
'/planes/$planId/asignaturas/nueva': typeof PlanesPlanIdDetalleAsignaturasNuevaRoute
|
||||
'/planes/$planId/asignaturas/': typeof PlanesPlanIdDetalleAsignaturasIndexRoute
|
||||
}
|
||||
export interface FileRoutesByTo {
|
||||
'/': typeof IndexRoute
|
||||
@@ -140,6 +140,7 @@ export interface FileRoutesByTo {
|
||||
'/demo/tanstack-query': typeof DemoTanstackQueryRoute
|
||||
'/planes': typeof PlanesListaRouteWithChildren
|
||||
'/planes/nuevo': typeof PlanesListaNuevoRoute
|
||||
'/planes/$planId/asignaturas': typeof PlanesPlanIdDetalleAsignaturasRouteWithChildren
|
||||
'/planes/$planId/documento': typeof PlanesPlanIdDetalleDocumentoRoute
|
||||
'/planes/$planId/flujo': typeof PlanesPlanIdDetalleFlujoRoute
|
||||
'/planes/$planId/historial': typeof PlanesPlanIdDetalleHistorialRoute
|
||||
@@ -148,7 +149,6 @@ export interface FileRoutesByTo {
|
||||
'/planes/$planId/asignaturas/$asignaturaId': typeof PlanesPlanIdAsignaturasAsignaturaIdRoute
|
||||
'/planes/$planId': typeof PlanesPlanIdDetalleIndexRoute
|
||||
'/planes/$planId/asignaturas/nueva': typeof PlanesPlanIdDetalleAsignaturasNuevaRoute
|
||||
'/planes/$planId/asignaturas': typeof PlanesPlanIdDetalleAsignaturasIndexRoute
|
||||
}
|
||||
export interface FileRoutesById {
|
||||
__root__: typeof rootRouteImport
|
||||
@@ -159,6 +159,7 @@ export interface FileRoutesById {
|
||||
'/planes/_lista': typeof PlanesListaRouteWithChildren
|
||||
'/planes/$planId/_detalle': typeof PlanesPlanIdDetalleRouteWithChildren
|
||||
'/planes/_lista/nuevo': typeof PlanesListaNuevoRoute
|
||||
'/planes/$planId/_detalle/asignaturas': typeof PlanesPlanIdDetalleAsignaturasRouteWithChildren
|
||||
'/planes/$planId/_detalle/documento': typeof PlanesPlanIdDetalleDocumentoRoute
|
||||
'/planes/$planId/_detalle/flujo': typeof PlanesPlanIdDetalleFlujoRoute
|
||||
'/planes/$planId/_detalle/historial': typeof PlanesPlanIdDetalleHistorialRoute
|
||||
@@ -167,7 +168,6 @@ export interface FileRoutesById {
|
||||
'/planes/$planId/asignaturas/$asignaturaId': typeof PlanesPlanIdAsignaturasAsignaturaIdRoute
|
||||
'/planes/$planId/_detalle/': typeof PlanesPlanIdDetalleIndexRoute
|
||||
'/planes/$planId/_detalle/asignaturas/nueva': typeof PlanesPlanIdDetalleAsignaturasNuevaRoute
|
||||
'/planes/$planId/_detalle/asignaturas/': typeof PlanesPlanIdDetalleAsignaturasIndexRoute
|
||||
}
|
||||
export interface FileRouteTypes {
|
||||
fileRoutesByFullPath: FileRoutesByFullPath
|
||||
@@ -179,6 +179,7 @@ export interface FileRouteTypes {
|
||||
| '/planes'
|
||||
| '/planes/$planId'
|
||||
| '/planes/nuevo'
|
||||
| '/planes/$planId/asignaturas'
|
||||
| '/planes/$planId/documento'
|
||||
| '/planes/$planId/flujo'
|
||||
| '/planes/$planId/historial'
|
||||
@@ -187,7 +188,6 @@ export interface FileRouteTypes {
|
||||
| '/planes/$planId/asignaturas/$asignaturaId'
|
||||
| '/planes/$planId/'
|
||||
| '/planes/$planId/asignaturas/nueva'
|
||||
| '/planes/$planId/asignaturas/'
|
||||
fileRoutesByTo: FileRoutesByTo
|
||||
to:
|
||||
| '/'
|
||||
@@ -196,6 +196,7 @@ export interface FileRouteTypes {
|
||||
| '/demo/tanstack-query'
|
||||
| '/planes'
|
||||
| '/planes/nuevo'
|
||||
| '/planes/$planId/asignaturas'
|
||||
| '/planes/$planId/documento'
|
||||
| '/planes/$planId/flujo'
|
||||
| '/planes/$planId/historial'
|
||||
@@ -204,7 +205,6 @@ export interface FileRouteTypes {
|
||||
| '/planes/$planId/asignaturas/$asignaturaId'
|
||||
| '/planes/$planId'
|
||||
| '/planes/$planId/asignaturas/nueva'
|
||||
| '/planes/$planId/asignaturas'
|
||||
id:
|
||||
| '__root__'
|
||||
| '/'
|
||||
@@ -214,6 +214,7 @@ export interface FileRouteTypes {
|
||||
| '/planes/_lista'
|
||||
| '/planes/$planId/_detalle'
|
||||
| '/planes/_lista/nuevo'
|
||||
| '/planes/$planId/_detalle/asignaturas'
|
||||
| '/planes/$planId/_detalle/documento'
|
||||
| '/planes/$planId/_detalle/flujo'
|
||||
| '/planes/$planId/_detalle/historial'
|
||||
@@ -222,7 +223,6 @@ export interface FileRouteTypes {
|
||||
| '/planes/$planId/asignaturas/$asignaturaId'
|
||||
| '/planes/$planId/_detalle/'
|
||||
| '/planes/$planId/_detalle/asignaturas/nueva'
|
||||
| '/planes/$planId/_detalle/asignaturas/'
|
||||
fileRoutesById: FileRoutesById
|
||||
}
|
||||
export interface RootRouteChildren {
|
||||
@@ -335,19 +335,19 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof PlanesPlanIdDetalleDocumentoRouteImport
|
||||
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 PlanesPlanIdDetalleAsignaturasIndexRouteImport
|
||||
fullPath: '/planes/$planId/asignaturas'
|
||||
preLoaderRoute: typeof PlanesPlanIdDetalleAsignaturasRouteImport
|
||||
parentRoute: typeof PlanesPlanIdDetalleRoute
|
||||
}
|
||||
'/planes/$planId/_detalle/asignaturas/nueva': {
|
||||
id: '/planes/$planId/_detalle/asignaturas/nueva'
|
||||
path: '/asignaturas/nueva'
|
||||
path: '/nueva'
|
||||
fullPath: '/planes/$planId/asignaturas/nueva'
|
||||
preLoaderRoute: typeof PlanesPlanIdDetalleAsignaturasNuevaRouteImport
|
||||
parentRoute: typeof PlanesPlanIdDetalleRoute
|
||||
parentRoute: typeof PlanesPlanIdDetalleAsignaturasRoute
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -364,28 +364,40 @@ const PlanesListaRouteWithChildren = PlanesListaRoute._addFileChildren(
|
||||
PlanesListaRouteChildren,
|
||||
)
|
||||
|
||||
interface PlanesPlanIdDetalleAsignaturasRouteChildren {
|
||||
PlanesPlanIdDetalleAsignaturasNuevaRoute: typeof PlanesPlanIdDetalleAsignaturasNuevaRoute
|
||||
}
|
||||
|
||||
const PlanesPlanIdDetalleAsignaturasRouteChildren: PlanesPlanIdDetalleAsignaturasRouteChildren =
|
||||
{
|
||||
PlanesPlanIdDetalleAsignaturasNuevaRoute:
|
||||
PlanesPlanIdDetalleAsignaturasNuevaRoute,
|
||||
}
|
||||
|
||||
const PlanesPlanIdDetalleAsignaturasRouteWithChildren =
|
||||
PlanesPlanIdDetalleAsignaturasRoute._addFileChildren(
|
||||
PlanesPlanIdDetalleAsignaturasRouteChildren,
|
||||
)
|
||||
|
||||
interface PlanesPlanIdDetalleRouteChildren {
|
||||
PlanesPlanIdDetalleAsignaturasRoute: typeof PlanesPlanIdDetalleAsignaturasRouteWithChildren
|
||||
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 PlanesPlanIdDetalleRouteChildren: PlanesPlanIdDetalleRouteChildren = {
|
||||
PlanesPlanIdDetalleAsignaturasRoute:
|
||||
PlanesPlanIdDetalleAsignaturasRouteWithChildren,
|
||||
PlanesPlanIdDetalleDocumentoRoute: PlanesPlanIdDetalleDocumentoRoute,
|
||||
PlanesPlanIdDetalleFlujoRoute: PlanesPlanIdDetalleFlujoRoute,
|
||||
PlanesPlanIdDetalleHistorialRoute: PlanesPlanIdDetalleHistorialRoute,
|
||||
PlanesPlanIdDetalleIaplanRoute: PlanesPlanIdDetalleIaplanRoute,
|
||||
PlanesPlanIdDetalleMapaRoute: PlanesPlanIdDetalleMapaRoute,
|
||||
PlanesPlanIdDetalleIndexRoute: PlanesPlanIdDetalleIndexRoute,
|
||||
PlanesPlanIdDetalleAsignaturasNuevaRoute:
|
||||
PlanesPlanIdDetalleAsignaturasNuevaRoute,
|
||||
PlanesPlanIdDetalleAsignaturasIndexRoute:
|
||||
PlanesPlanIdDetalleAsignaturasIndexRoute,
|
||||
}
|
||||
|
||||
const PlanesPlanIdDetalleRouteWithChildren =
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createFileRoute, useNavigate } from '@tanstack/react-router'
|
||||
import { createFileRoute, Outlet, useNavigate } from '@tanstack/react-router'
|
||||
import {
|
||||
Plus,
|
||||
Copy,
|
||||
@@ -72,7 +72,7 @@ const mapAsignaturas = (asigApi: Array<any> = []): Array<Asignatura> => {
|
||||
}))
|
||||
}
|
||||
|
||||
export const Route = createFileRoute('/planes/$planId/_detalle/asignaturas/')({
|
||||
export const Route = createFileRoute('/planes/$planId/_detalle/asignaturas')({
|
||||
component: AsignaturasPage,
|
||||
})
|
||||
|
||||
@@ -306,6 +306,7 @@ function AsignaturasPage() {
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
<Outlet />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user