Merge branch 'main' into issue/53-guardado-automtico
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -8,3 +8,4 @@ count.txt
|
|||||||
.nitro
|
.nitro
|
||||||
.tanstack
|
.tanstack
|
||||||
.wrangler
|
.wrangler
|
||||||
|
diff.txt
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import type {
|
import { useEffect, useState } from 'react'
|
||||||
NewSubjectWizardState,
|
|
||||||
TipoAsignatura,
|
import type { NewSubjectWizardState } from '@/features/asignaturas/nueva/types'
|
||||||
} from '@/features/asignaturas/nueva/types'
|
import type { Database } from '@/types/supabase'
|
||||||
|
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { Label } from '@/components/ui/label'
|
import { Label } from '@/components/ui/label'
|
||||||
@@ -12,10 +12,9 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@/components/ui/select'
|
} from '@/components/ui/select'
|
||||||
import {
|
import { useSubjectEstructuras } from '@/data'
|
||||||
ESTRUCTURAS_SEP,
|
import { TIPOS_MATERIA } from '@/features/asignaturas/nueva/catalogs'
|
||||||
TIPOS_MATERIA,
|
import { cn } from '@/lib/utils'
|
||||||
} from '@/features/asignaturas/nueva/catalogs'
|
|
||||||
|
|
||||||
export function PasoBasicosForm({
|
export function PasoBasicosForm({
|
||||||
wizard,
|
wizard,
|
||||||
@@ -24,6 +23,20 @@ export function PasoBasicosForm({
|
|||||||
wizard: NewSubjectWizardState
|
wizard: NewSubjectWizardState
|
||||||
onChange: React.Dispatch<React.SetStateAction<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 (
|
return (
|
||||||
<div className="grid gap-4 sm:grid-cols-2">
|
<div className="grid gap-4 sm:grid-cols-2">
|
||||||
<div className="grid gap-1 sm:col-span-2">
|
<div className="grid gap-1 sm:col-span-2">
|
||||||
@@ -33,45 +46,66 @@ export function PasoBasicosForm({
|
|||||||
placeholder="Ej. Matemáticas Discretas"
|
placeholder="Ej. Matemáticas Discretas"
|
||||||
value={wizard.datosBasicos.nombre}
|
value={wizard.datosBasicos.nombre}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
onChange((w) => ({
|
onChange(
|
||||||
|
(w): NewSubjectWizardState => ({
|
||||||
...w,
|
...w,
|
||||||
datosBasicos: { ...w.datosBasicos, nombre: e.target.value },
|
datosBasicos: { ...w.datosBasicos, nombre: e.target.value },
|
||||||
}))
|
}),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
className="placeholder:text-muted-foreground/70 font-medium not-italic placeholder:font-normal placeholder:italic"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-1">
|
<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
|
<Input
|
||||||
id="clave"
|
id="codigo"
|
||||||
placeholder="Ej. MAT-101"
|
placeholder="Ej. MAT-101"
|
||||||
value={wizard.datosBasicos.clave || ''}
|
value={wizard.datosBasicos.codigo || ''}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
onChange((w) => ({
|
onChange(
|
||||||
|
(w): NewSubjectWizardState => ({
|
||||||
...w,
|
...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>
|
||||||
|
|
||||||
<div className="grid gap-1">
|
<div className="grid gap-1">
|
||||||
<Label htmlFor="tipo">Tipo</Label>
|
<Label htmlFor="tipo">Tipo</Label>
|
||||||
<Select
|
<Select
|
||||||
value={wizard.datosBasicos.tipo}
|
value={(wizard.datosBasicos.tipo ?? '') as string}
|
||||||
onValueChange={(val) =>
|
onValueChange={(value: string) =>
|
||||||
onChange((w) => ({
|
onChange(
|
||||||
|
(w): NewSubjectWizardState => ({
|
||||||
...w,
|
...w,
|
||||||
datosBasicos: { ...w.datosBasicos, tipo: val as TipoAsignatura },
|
datosBasicos: {
|
||||||
}))
|
...w.datosBasicos,
|
||||||
|
tipo: value as NewSubjectWizardState['datosBasicos']['tipo'],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<SelectTrigger
|
<SelectTrigger
|
||||||
id="tipo"
|
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>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{TIPOS_MATERIA.map((t) => (
|
{TIPOS_MATERIA.map((t) => (
|
||||||
@@ -87,49 +121,175 @@ export function PasoBasicosForm({
|
|||||||
<Label htmlFor="creditos">Créditos</Label>
|
<Label htmlFor="creditos">Créditos</Label>
|
||||||
<Input
|
<Input
|
||||||
id="creditos"
|
id="creditos"
|
||||||
type="number"
|
type="text"
|
||||||
min={0}
|
inputMode="decimal"
|
||||||
value={wizard.datosBasicos.creditos}
|
pattern="^\\d*(?:[.,]\\d{0,2})?$"
|
||||||
onChange={(e) =>
|
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) => ({
|
onChange((w) => ({
|
||||||
...w,
|
...w,
|
||||||
datosBasicos: {
|
datosBasicos: {
|
||||||
...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>
|
||||||
|
|
||||||
<div className="grid gap-1">
|
<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
|
<Input
|
||||||
id="horas"
|
id="horasAcademicas"
|
||||||
type="number"
|
type="number"
|
||||||
min={0}
|
min={1}
|
||||||
value={wizard.datosBasicos.horasSemana || 0}
|
step={1}
|
||||||
onChange={(e) =>
|
inputMode="numeric"
|
||||||
onChange((w) => ({
|
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,
|
...w,
|
||||||
datosBasicos: {
|
datosBasicos: {
|
||||||
...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>
|
||||||
|
|
||||||
<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>
|
<Label htmlFor="estructura">Estructura de la asignatura</Label>
|
||||||
<Select
|
<Select
|
||||||
value={wizard.datosBasicos.estructuraId}
|
value={wizard.datosBasicos.estructuraId as string}
|
||||||
onValueChange={(val) =>
|
onValueChange={(val) =>
|
||||||
onChange((w) => ({
|
onChange(
|
||||||
|
(w): NewSubjectWizardState => ({
|
||||||
...w,
|
...w,
|
||||||
datosBasicos: { ...w.datosBasicos, estructuraId: val },
|
datosBasicos: { ...w.datosBasicos, estructuraId: val },
|
||||||
}))
|
}),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<SelectTrigger
|
<SelectTrigger
|
||||||
@@ -139,11 +299,15 @@ export function PasoBasicosForm({
|
|||||||
<SelectValue placeholder="Selecciona plantilla..." />
|
<SelectValue placeholder="Selecciona plantilla..." />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{ESTRUCTURAS_SEP.map((e) => (
|
{estructuras?.map(
|
||||||
|
(
|
||||||
|
e: Database['public']['Tables']['estructuras_asignatura']['Row'],
|
||||||
|
) => (
|
||||||
<SelectItem key={e.id} value={e.id}>
|
<SelectItem key={e.id} value={e.id}>
|
||||||
{e.label}
|
{e.nombre}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
),
|
||||||
|
)}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<p className="text-muted-foreground text-xs">
|
<p className="text-muted-foreground text-xs">
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import * as Icons from 'lucide-react'
|
import * as Icons from 'lucide-react'
|
||||||
|
|
||||||
|
import type { UploadedFile } from '@/components/planes/wizard/PasoDetallesPanel/FileDropZone'
|
||||||
import type { NewSubjectWizardState } from '@/features/asignaturas/nueva/types'
|
import type { NewSubjectWizardState } from '@/features/asignaturas/nueva/types'
|
||||||
|
|
||||||
import { Button } from '@/components/ui/button'
|
import ReferenciasParaIA from '@/components/planes/wizard/PasoDetallesPanel/ReferenciasParaIA'
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
|
||||||
CardDescription,
|
CardDescription,
|
||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
@@ -21,22 +21,21 @@ import {
|
|||||||
} from '@/components/ui/select'
|
} from '@/components/ui/select'
|
||||||
import { Textarea } from '@/components/ui/textarea'
|
import { Textarea } from '@/components/ui/textarea'
|
||||||
import {
|
import {
|
||||||
ARCHIVOS_SISTEMA_MOCK,
|
|
||||||
FACULTADES,
|
FACULTADES,
|
||||||
MATERIAS_MOCK,
|
MATERIAS_MOCK,
|
||||||
PLANES_MOCK,
|
PLANES_MOCK,
|
||||||
} from '@/features/asignaturas/nueva/catalogs'
|
} from '@/features/asignaturas/nueva/catalogs'
|
||||||
|
|
||||||
export function PasoConfiguracionPanel({
|
export function PasoDetallesPanel({
|
||||||
wizard,
|
wizard,
|
||||||
onChange,
|
onChange,
|
||||||
onGenerarIA,
|
onGenerarIA: _onGenerarIA,
|
||||||
}: {
|
}: {
|
||||||
wizard: NewSubjectWizardState
|
wizard: NewSubjectWizardState
|
||||||
onChange: React.Dispatch<React.SetStateAction<NewSubjectWizardState>>
|
onChange: React.Dispatch<React.SetStateAction<NewSubjectWizardState>>
|
||||||
onGenerarIA: () => void
|
onGenerarIA: () => void
|
||||||
}) {
|
}) {
|
||||||
if (wizard.modoCreacion === 'MANUAL') {
|
if (wizard.tipoOrigen === 'MANUAL') {
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
@@ -50,116 +49,104 @@ export function PasoConfiguracionPanel({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (wizard.modoCreacion === 'IA') {
|
if (wizard.tipoOrigen === 'IA') {
|
||||||
return (
|
return (
|
||||||
<div className="grid gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<div className="grid gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<Label>Descripción del enfoque</Label>
|
<Label>Descripción del enfoque académico</Label>
|
||||||
<Textarea
|
<Textarea
|
||||||
placeholder="Ej. Asignatura teórica-práctica enfocada en patrones de diseño..."
|
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?.descripcionEnfoque}
|
value={wizard.iaConfig?.descripcionEnfoqueAcademico}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
onChange((w) => ({
|
onChange(
|
||||||
|
(w): NewSubjectWizardState => ({
|
||||||
...w,
|
...w,
|
||||||
iaConfig: {
|
iaConfig: {
|
||||||
...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>
|
||||||
<div className="grid gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<Label>Notas adicionales</Label>
|
<Label>
|
||||||
<Textarea
|
Instrucciones adicionales para la IA
|
||||||
placeholder="Restricciones, bibliografía sugerida, etc."
|
<span className="text-xs font-normal text-gray-500 dark:text-gray-400">
|
||||||
value={wizard.iaConfig?.notasAdicionales}
|
(Opcional)
|
||||||
onChange={(e) =>
|
</span>
|
||||||
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}
|
|
||||||
</Label>
|
</Label>
|
||||||
</div>
|
<Textarea
|
||||||
))}
|
placeholder="Opcional: restricciones y preferencias. Ej.: incluye bibliografía en español, evita contenido avanzado, prioriza evaluación por proyectos."
|
||||||
</div>
|
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>
|
||||||
|
|
||||||
<div className="flex justify-end">
|
<ReferenciasParaIA
|
||||||
<Button onClick={onGenerarIA} disabled={wizard.isLoading}>
|
selectedArchivoIds={wizard.iaConfig?.archivosReferencia || []}
|
||||||
{wizard.isLoading ? (
|
selectedRepositorioIds={wizard.iaConfig?.repositoriosReferencia || []}
|
||||||
<>
|
uploadedFiles={wizard.iaConfig?.archivosAdjuntos || []}
|
||||||
<Icons.Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
onToggleArchivo={(id, checked) =>
|
||||||
Generando...
|
onChange((w): NewSubjectWizardState => {
|
||||||
</>
|
const prev = w.iaConfig?.archivosReferencia || []
|
||||||
) : (
|
const next = checked
|
||||||
<>
|
? [...prev, id]
|
||||||
<Icons.Sparkles className="mr-2 h-4 w-4" /> Generar Preview
|
: prev.filter((a) => a !== id)
|
||||||
</>
|
return {
|
||||||
)}
|
...w,
|
||||||
</Button>
|
iaConfig: {
|
||||||
</div>
|
...w.iaConfig!,
|
||||||
|
archivosReferencia: next,
|
||||||
{wizard.resumen.previewAsignatura && (
|
},
|
||||||
<Card className="bg-muted/50 border-dashed">
|
}
|
||||||
<CardHeader>
|
})
|
||||||
<CardTitle className="text-base">Vista previa generada</CardTitle>
|
}
|
||||||
</CardHeader>
|
onToggleRepositorio={(id, checked) =>
|
||||||
<CardContent className="text-muted-foreground text-sm">
|
onChange((w): NewSubjectWizardState => {
|
||||||
<p>
|
const prev = w.iaConfig?.repositoriosReferencia || []
|
||||||
<strong>Objetivo:</strong>{' '}
|
const next = checked
|
||||||
{wizard.resumen.previewAsignatura.objetivo}
|
? [...prev, id]
|
||||||
</p>
|
: prev.filter((r) => r !== id)
|
||||||
<p className="mt-2">
|
return {
|
||||||
Se detectaron {wizard.resumen.previewAsignatura.unidades}{' '}
|
...w,
|
||||||
unidades temáticas y{' '}
|
iaConfig: {
|
||||||
{wizard.resumen.previewAsignatura.bibliografiaCount} fuentes
|
...w.iaConfig!,
|
||||||
bibliográficas.
|
repositoriosReferencia: next,
|
||||||
</p>
|
},
|
||||||
</CardContent>
|
}
|
||||||
</Card>
|
})
|
||||||
)}
|
}
|
||||||
|
onFilesChange={(files: Array<UploadedFile>) =>
|
||||||
|
onChange(
|
||||||
|
(w): NewSubjectWizardState => ({
|
||||||
|
...w,
|
||||||
|
iaConfig: {
|
||||||
|
...w.iaConfig!,
|
||||||
|
archivosAdjuntos: files,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (wizard.subModoClonado === 'INTERNO') {
|
if (wizard.tipoOrigen === 'CLONADO_INTERNO') {
|
||||||
return (
|
return (
|
||||||
<div className="grid gap-4">
|
<div className="grid gap-4">
|
||||||
<div className="grid gap-2 sm:grid-cols-3">
|
<div className="grid gap-2 sm:grid-cols-3">
|
||||||
@@ -217,12 +204,22 @@ export function PasoConfiguracionPanel({
|
|||||||
{MATERIAS_MOCK.map((m) => (
|
{MATERIAS_MOCK.map((m) => (
|
||||||
<div
|
<div
|
||||||
key={m.id}
|
key={m.id}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
onChange((w) => ({
|
onChange((w) => ({
|
||||||
...w,
|
...w,
|
||||||
clonInterno: { ...w.clonInterno, asignaturaOrigenId: m.id },
|
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 ${
|
className={`hover:bg-accent flex cursor-pointer items-center justify-between rounded-md border p-3 ${
|
||||||
wizard.clonInterno?.asignaturaOrigenId === m.id
|
wizard.clonInterno?.asignaturaOrigenId === m.id
|
||||||
? 'border-primary bg-primary/5 ring-primary ring-1'
|
? '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 (
|
return (
|
||||||
<div className="grid gap-4">
|
<div className="grid gap-4">
|
||||||
<div className="rounded-lg border border-dashed p-8 text-center">
|
<div className="rounded-lg border border-dashed p-8 text-center">
|
||||||
@@ -1,10 +1,6 @@
|
|||||||
import * as Icons from 'lucide-react'
|
import * as Icons from 'lucide-react'
|
||||||
|
|
||||||
import type {
|
import type { NewSubjectWizardState } from '@/features/asignaturas/nueva/types'
|
||||||
ModoCreacion,
|
|
||||||
NewSubjectWizardState,
|
|
||||||
SubModoClonado,
|
|
||||||
} from '@/features/asignaturas/nueva/types'
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
@@ -21,19 +17,33 @@ export function PasoMetodoCardGroup({
|
|||||||
wizard: NewSubjectWizardState
|
wizard: NewSubjectWizardState
|
||||||
onChange: React.Dispatch<React.SetStateAction<NewSubjectWizardState>>
|
onChange: React.Dispatch<React.SetStateAction<NewSubjectWizardState>>
|
||||||
}) {
|
}) {
|
||||||
const isSelected = (m: ModoCreacion) => wizard.modoCreacion === m
|
const isSelected = (modo: NewSubjectWizardState['tipoOrigen']) =>
|
||||||
const isSubSelected = (s: SubModoClonado) => wizard.subModoClonado === s
|
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 (
|
return (
|
||||||
<div className="grid gap-4 sm:grid-cols-3">
|
<div className="grid gap-4 sm:grid-cols-3">
|
||||||
<Card
|
<Card
|
||||||
className={isSelected('MANUAL') ? 'ring-ring ring-2' : ''}
|
className={isSelected('MANUAL') ? 'ring-ring ring-2' : ''}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
onChange((w) => ({
|
onChange(
|
||||||
|
(w): NewSubjectWizardState => ({
|
||||||
...w,
|
...w,
|
||||||
modoCreacion: 'MANUAL',
|
tipoOrigen: 'MANUAL',
|
||||||
subModoClonado: undefined,
|
}),
|
||||||
}))
|
)
|
||||||
}
|
}
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
@@ -51,11 +61,12 @@ export function PasoMetodoCardGroup({
|
|||||||
<Card
|
<Card
|
||||||
className={isSelected('IA') ? 'ring-ring ring-2' : ''}
|
className={isSelected('IA') ? 'ring-ring ring-2' : ''}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
onChange((w) => ({
|
onChange(
|
||||||
|
(w): NewSubjectWizardState => ({
|
||||||
...w,
|
...w,
|
||||||
modoCreacion: 'IA',
|
tipoOrigen: 'IA',
|
||||||
subModoClonado: undefined,
|
}),
|
||||||
}))
|
)
|
||||||
}
|
}
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
@@ -69,8 +80,10 @@ export function PasoMetodoCardGroup({
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card
|
<Card
|
||||||
className={isSelected('CLONADO') ? 'ring-ring ring-2' : ''}
|
className={isSelected('OTRO') ? 'ring-ring ring-2' : ''}
|
||||||
onClick={() => onChange((w) => ({ ...w, modoCreacion: 'CLONADO' }))}
|
onClick={() =>
|
||||||
|
onChange((w): NewSubjectWizardState => ({ ...w, tipoOrigen: 'OTRO' }))
|
||||||
|
}
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
@@ -80,18 +93,34 @@ export function PasoMetodoCardGroup({
|
|||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription>De otra asignatura o archivo Word.</CardDescription>
|
<CardDescription>De otra asignatura o archivo Word.</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
{wizard.modoCreacion === 'CLONADO' && (
|
{(wizard.tipoOrigen === 'OTRO' ||
|
||||||
<CardContent>
|
wizard.tipoOrigen === 'CLONADO_INTERNO' ||
|
||||||
<div className="flex flex-col gap-3">
|
wizard.tipoOrigen === 'CLONADO_TRADICIONAL') && (
|
||||||
|
<CardContent className="flex flex-col gap-3">
|
||||||
<div
|
<div
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation()
|
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 ${
|
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'
|
? 'bg-primary/5 text-primary ring-primary border-primary ring-1'
|
||||||
: 'border-border text-muted-foreground'
|
: 'border-border text-muted-foreground'
|
||||||
}`}
|
}`}
|
||||||
@@ -110,10 +139,25 @@ export function PasoMetodoCardGroup({
|
|||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation()
|
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 ${
|
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'
|
? 'bg-primary/5 text-primary ring-primary border-primary ring-1'
|
||||||
: 'border-border text-muted-foreground'
|
: 'border-border text-muted-foreground'
|
||||||
}`}
|
}`}
|
||||||
@@ -121,10 +165,7 @@ export function PasoMetodoCardGroup({
|
|||||||
<Icons.Upload className="h-6 w-6 flex-none" />
|
<Icons.Upload className="h-6 w-6 flex-none" />
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<span className="text-sm font-medium">Desde archivos</span>
|
<span className="text-sm font-medium">Desde archivos</span>
|
||||||
<span className="text-xs opacity-70">
|
<span className="text-xs opacity-70">Subir Word existente</span>
|
||||||
Subir Word existente
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@@ -9,9 +9,38 @@ import {
|
|||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from '@/components/ui/card'
|
} 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 }) {
|
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 (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
@@ -20,53 +49,145 @@ export function PasoResumenCard({ wizard }: { wizard: NewSubjectWizardState }) {
|
|||||||
Verifica los datos antes de crear la asignatura.
|
Verifica los datos antes de crear la asignatura.
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="grid gap-4 text-sm">
|
<CardContent>
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid gap-4 text-sm">
|
||||||
|
<div className="grid gap-2">
|
||||||
<div>
|
<div>
|
||||||
<span className="text-muted-foreground">Nombre:</span>
|
<span className="text-muted-foreground">Plan de estudios: </span>
|
||||||
<div className="font-medium">{wizard.datosBasicos.nombre}</div>
|
<span className="font-medium">
|
||||||
|
{plan?.nombre || wizard.plan_estudio_id || '—'}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
{plan?.carreras?.nombre ? (
|
||||||
<div>
|
<div>
|
||||||
<span className="text-muted-foreground">Tipo:</span>
|
<span className="text-muted-foreground">Carrera: </span>
|
||||||
<div className="font-medium">{wizard.datosBasicos.tipo}</div>
|
<span className="font-medium">{plan.carreras.nombre}</span>
|
||||||
</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>
|
|
||||||
</div>
|
</div>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-muted rounded-md p-3">
|
<div className="bg-muted rounded-md p-3">
|
||||||
<span className="text-muted-foreground">Modo de creación:</span>
|
<span className="text-muted-foreground">Tipo de origen: </span>
|
||||||
<div className="flex items-center gap-2 font-medium">
|
<span className="inline-flex items-center gap-2 font-medium">
|
||||||
{wizard.modoCreacion === 'MANUAL' && (
|
{wizard.tipoOrigen === 'MANUAL' && (
|
||||||
<>
|
<Icons.Pencil className="h-4 w-4" />
|
||||||
<Icons.Pencil className="h-4 w-4" /> Manual (Vacía)
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
{wizard.modoCreacion === 'IA' && (
|
{wizard.tipoOrigen === 'IA' && (
|
||||||
<>
|
<Icons.Sparkles className="h-4 w-4" />
|
||||||
<Icons.Sparkles className="h-4 w-4" /> Generada con IA
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
{wizard.modoCreacion === 'CLONADO' && (
|
{(wizard.tipoOrigen === 'CLONADO_INTERNO' ||
|
||||||
<>
|
wizard.tipoOrigen === 'CLONADO_TRADICIONAL') && (
|
||||||
<Icons.Copy className="h-4 w-4" /> Clonada
|
<Icons.Copy className="h-4 w-4" />
|
||||||
{wizard.subModoClonado === 'INTERNO'
|
|
||||||
? ' (Sistema)'
|
|
||||||
: ' (Archivo)'}
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
|
{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>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@@ -1,64 +1,116 @@
|
|||||||
|
import { useNavigate } from '@tanstack/react-router'
|
||||||
|
|
||||||
|
import type { AIGenerateSubjectInput } from '@/data'
|
||||||
import type { NewSubjectWizardState } from '@/features/asignaturas/nueva/types'
|
import type { NewSubjectWizardState } from '@/features/asignaturas/nueva/types'
|
||||||
|
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { useGenerateSubjectAI } from '@/data'
|
||||||
|
|
||||||
export function WizardControls({
|
export function WizardControls({
|
||||||
Wizard,
|
|
||||||
methods,
|
|
||||||
wizard,
|
wizard,
|
||||||
canContinueDesdeMetodo,
|
setWizard,
|
||||||
canContinueDesdeBasicos,
|
errorMessage,
|
||||||
canContinueDesdeConfig,
|
onPrev,
|
||||||
onCreate,
|
onNext,
|
||||||
|
disablePrev,
|
||||||
|
disableNext,
|
||||||
|
disableCreate,
|
||||||
|
isLastStep,
|
||||||
}: {
|
}: {
|
||||||
Wizard: any
|
|
||||||
methods: any
|
|
||||||
wizard: NewSubjectWizardState
|
wizard: NewSubjectWizardState
|
||||||
canContinueDesdeMetodo: boolean
|
setWizard: React.Dispatch<React.SetStateAction<NewSubjectWizardState>>
|
||||||
canContinueDesdeBasicos: boolean
|
errorMessage?: string | null
|
||||||
canContinueDesdeConfig: boolean
|
onPrev: () => void
|
||||||
onCreate: () => void
|
onNext: () => void
|
||||||
|
disablePrev: boolean
|
||||||
|
disableNext: boolean
|
||||||
|
disableCreate: boolean
|
||||||
|
isLastStep: boolean
|
||||||
}) {
|
}) {
|
||||||
const idx = Wizard.utils.getIndex(methods.current.id)
|
const navigate = useNavigate()
|
||||||
const isLast = idx >= Wizard.steps.length - 1
|
const generateSubjectAI = useGenerateSubjectAI()
|
||||||
|
const handleCreate = async () => {
|
||||||
|
setWizard((w) => ({
|
||||||
|
...w,
|
||||||
|
isLoading: true,
|
||||||
|
errorMessage: null,
|
||||||
|
}))
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (wizard.tipoOrigen === 'IA') {
|
||||||
|
const aiInput: AIGenerateSubjectInput = {
|
||||||
|
plan_estudio_id: wizard.plan_estudio_id,
|
||||||
|
datosBasicos: {
|
||||||
|
nombre: wizard.datosBasicos.nombre,
|
||||||
|
codigo: wizard.datosBasicos.codigo,
|
||||||
|
tipo: wizard.datosBasicos.tipo!,
|
||||||
|
creditos: wizard.datosBasicos.creditos!,
|
||||||
|
horasIndependientes: wizard.datosBasicos.horasIndependientes,
|
||||||
|
horasAcademicas: wizard.datosBasicos.horasAcademicas,
|
||||||
|
estructuraId: wizard.datosBasicos.estructuraId!,
|
||||||
|
},
|
||||||
|
iaConfig: {
|
||||||
|
descripcionEnfoqueAcademico:
|
||||||
|
wizard.iaConfig!.descripcionEnfoqueAcademico,
|
||||||
|
instruccionesAdicionalesIA:
|
||||||
|
wizard.iaConfig!.instruccionesAdicionalesIA,
|
||||||
|
archivosReferencia: wizard.iaConfig!.archivosReferencia,
|
||||||
|
repositoriosReferencia:
|
||||||
|
wizard.iaConfig!.repositoriosReferencia || [],
|
||||||
|
archivosAdjuntos: wizard.iaConfig!.archivosAdjuntos || [],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`${new Date().toISOString()} - Enviando a generar asignatura con IA`,
|
||||||
|
)
|
||||||
|
|
||||||
|
const asignatura = await generateSubjectAI.mutateAsync(aiInput)
|
||||||
|
console.log(
|
||||||
|
`${new Date().toISOString()} - Asignatura IA generada`,
|
||||||
|
asignatura,
|
||||||
|
)
|
||||||
|
|
||||||
|
navigate({
|
||||||
|
to: `/planes/${wizard.plan_estudio_id}/asignaturas/${asignatura.id}`,
|
||||||
|
state: { showConfetti: true },
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
setWizard((w) => ({
|
||||||
|
...w,
|
||||||
|
isLoading: false,
|
||||||
|
errorMessage: err?.message ?? 'Error creando la asignatura',
|
||||||
|
}))
|
||||||
|
} finally {
|
||||||
|
setWizard((w) => ({ ...w, isLoading: false }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
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">
|
<div className="flex-1">
|
||||||
{wizard.errorMessage && (
|
{(errorMessage ?? wizard.errorMessage) && (
|
||||||
<span className="text-destructive text-sm font-medium">
|
<span className="text-destructive text-sm font-medium">
|
||||||
{wizard.errorMessage}
|
{errorMessage ?? wizard.errorMessage}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex gap-4">
|
{isLastStep ? (
|
||||||
<Button
|
<Button onClick={handleCreate} disabled={disableCreate}>
|
||||||
variant="secondary"
|
{wizard.isLoading ? 'Creando...' : 'Crear Asignatura'}
|
||||||
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
|
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<Button onClick={onCreate} disabled={wizard.isLoading}>
|
<Button onClick={onNext} disabled={disableNext}>
|
||||||
{wizard.isLoading ? 'Creando...' : 'Crear Asignatura'}
|
Siguiente
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -246,9 +246,9 @@ export function PasoBasicosForm({
|
|||||||
// Keep undefined when the input is empty so the field stays optional
|
// Keep undefined when the input is empty so the field stays optional
|
||||||
numCiclos: (() => {
|
numCiclos: (() => {
|
||||||
const raw = e.target.value
|
const raw = e.target.value
|
||||||
if (raw === '') return undefined
|
if (raw === '') return null
|
||||||
const asNumber = Number(raw)
|
const asNumber = Number(raw)
|
||||||
if (Number.isNaN(asNumber)) return undefined
|
if (Number.isNaN(asNumber)) return null
|
||||||
// Coerce to positive integer (natural numbers without zero)
|
// Coerce to positive integer (natural numbers without zero)
|
||||||
const n = Math.floor(Math.abs(asNumber))
|
const n = Math.floor(Math.abs(asNumber))
|
||||||
return n >= 1 ? n : 1
|
return n >= 1 ? n : 1
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ export function PasoDetallesPanel({
|
|||||||
selectedRepositorioIds={wizard.iaConfig?.repositoriosReferencia || []}
|
selectedRepositorioIds={wizard.iaConfig?.repositoriosReferencia || []}
|
||||||
uploadedFiles={wizard.iaConfig?.archivosAdjuntos || []}
|
uploadedFiles={wizard.iaConfig?.archivosAdjuntos || []}
|
||||||
onToggleArchivo={(id, checked) =>
|
onToggleArchivo={(id, checked) =>
|
||||||
onChange((w) => {
|
onChange((w): NewPlanWizardState => {
|
||||||
const prev = w.iaConfig?.archivosReferencia || []
|
const prev = w.iaConfig?.archivosReferencia || []
|
||||||
const next = checked
|
const next = checked
|
||||||
? [...prev, id]
|
? [...prev, id]
|
||||||
@@ -105,7 +105,7 @@ export function PasoDetallesPanel({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
onToggleRepositorio={(id, checked) =>
|
onToggleRepositorio={(id, checked) =>
|
||||||
onChange((w) => {
|
onChange((w): NewPlanWizardState => {
|
||||||
const prev = w.iaConfig?.repositoriosReferencia || []
|
const prev = w.iaConfig?.repositoriosReferencia || []
|
||||||
const next = checked
|
const next = checked
|
||||||
? [...prev, id]
|
? [...prev, id]
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useNavigate } from '@tanstack/react-router'
|
import { useNavigate } from '@tanstack/react-router'
|
||||||
|
|
||||||
|
import type { AIGeneratePlanInput } from '@/data'
|
||||||
import type { NivelPlanEstudio, TipoCiclo } from '@/data/types/domain'
|
import type { NivelPlanEstudio, TipoCiclo } from '@/data/types/domain'
|
||||||
import type { NewPlanWizardState } from '@/features/planes/nuevo/types'
|
import type { NewPlanWizardState } from '@/features/planes/nuevo/types'
|
||||||
// import type { Database } from '@/types/supabase'
|
// import type { Database } from '@/types/supabase'
|
||||||
@@ -54,11 +55,11 @@ export function WizardControls({
|
|||||||
? wizard.datosBasicos.numCiclos
|
? wizard.datosBasicos.numCiclos
|
||||||
: 1
|
: 1
|
||||||
|
|
||||||
const aiInput = {
|
const aiInput: AIGeneratePlanInput = {
|
||||||
datosBasicos: {
|
datosBasicos: {
|
||||||
nombrePlan: wizard.datosBasicos.nombrePlan,
|
nombrePlan: wizard.datosBasicos.nombrePlan,
|
||||||
carreraId: wizard.datosBasicos.carrera.id || undefined,
|
carreraId: wizard.datosBasicos.carrera.id,
|
||||||
facultadId: wizard.datosBasicos.facultad.id || undefined,
|
facultadId: wizard.datosBasicos.facultad.id,
|
||||||
nivel: wizard.datosBasicos.nivel as string,
|
nivel: wizard.datosBasicos.nivel as string,
|
||||||
tipoCiclo: tipoCicloSafe,
|
tipoCiclo: tipoCicloSafe,
|
||||||
numCiclos: numCiclosSafe,
|
numCiclos: numCiclosSafe,
|
||||||
@@ -77,11 +78,11 @@ export function WizardControls({
|
|||||||
|
|
||||||
console.log(`${new Date().toISOString()} - Enviando a generar plan IA`)
|
console.log(`${new Date().toISOString()} - Enviando a generar plan IA`)
|
||||||
|
|
||||||
const data = await generatePlanAI.mutateAsync(aiInput as any)
|
const plan = await generatePlanAI.mutateAsync(aiInput as any)
|
||||||
console.log(`${new Date().toISOString()} - Plan IA generado`, data)
|
console.log(`${new Date().toISOString()} - Plan IA generado`, plan)
|
||||||
|
|
||||||
navigate({
|
navigate({
|
||||||
to: `/planes/${data.plan.id}`,
|
to: `/planes/${plan.id}`,
|
||||||
state: { showConfetti: true },
|
state: { showConfetti: true },
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
@@ -122,7 +123,7 @@ export function WizardControls({
|
|||||||
<Button variant="secondary" onClick={onPrev} disabled={disablePrev}>
|
<Button variant="secondary" onClick={onPrev} disabled={disablePrev}>
|
||||||
Anterior
|
Anterior
|
||||||
</Button>
|
</Button>
|
||||||
<div className="flex-1">
|
<div className="mx-2 flex-1">
|
||||||
{errorMessage && (
|
{errorMessage && (
|
||||||
<span className="text-destructive text-sm font-medium">
|
<span className="text-destructive text-sm font-medium">
|
||||||
{errorMessage}
|
{errorMessage}
|
||||||
|
|||||||
@@ -268,8 +268,8 @@ export type AIGeneratePlanInput = {
|
|||||||
estructuraPlanId: UUID
|
estructuraPlanId: UUID
|
||||||
}
|
}
|
||||||
iaConfig: {
|
iaConfig: {
|
||||||
descripcionEnfoque: string
|
descripcionEnfoqueAcademico: string
|
||||||
notasAdicionales?: string
|
instruccionesAdicionalesIA?: string
|
||||||
archivosReferencia?: Array<UUID>
|
archivosReferencia?: Array<UUID>
|
||||||
repositoriosIds?: Array<UUID>
|
repositoriosIds?: Array<UUID>
|
||||||
archivosAdjuntos: Array<UploadedFile>
|
archivosAdjuntos: Array<UploadedFile>
|
||||||
|
|||||||
@@ -11,10 +11,12 @@ import type {
|
|||||||
TipoAsignatura,
|
TipoAsignatura,
|
||||||
UUID,
|
UUID,
|
||||||
} from '../types/domain'
|
} from '../types/domain'
|
||||||
|
import type { UploadedFile } from '@/components/planes/wizard/PasoDetallesPanel/FileDropZone'
|
||||||
|
import type { Database } from '@/types/supabase'
|
||||||
|
|
||||||
const EDGE = {
|
const EDGE = {
|
||||||
subjects_create_manual: 'subjects_create_manual',
|
subjects_create_manual: 'subjects_create_manual',
|
||||||
ai_generate_subject: 'ai_generate_subject',
|
ai_generate_subject: 'ai-generate-subject',
|
||||||
subjects_persist_from_ai: 'subjects_persist_from_ai',
|
subjects_persist_from_ai: 'subjects_persist_from_ai',
|
||||||
subjects_clone_from_existing: 'subjects_clone_from_existing',
|
subjects_clone_from_existing: 'subjects_clone_from_existing',
|
||||||
subjects_import_from_file: 'subjects_import_from_file',
|
subjects_import_from_file: 'subjects_import_from_file',
|
||||||
@@ -101,26 +103,58 @@ export async function subjects_create_manual(
|
|||||||
return invokeEdge<Asignatura>(EDGE.subjects_create_manual, payload)
|
return invokeEdge<Asignatura>(EDGE.subjects_create_manual, payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function ai_generate_subject(payload: {
|
export type AIGenerateSubjectInput = {
|
||||||
planId: UUID
|
plan_estudio_id: Asignatura['plan_estudio_id']
|
||||||
datosBasicos: {
|
datosBasicos: {
|
||||||
nombre: string
|
nombre: Asignatura['nombre']
|
||||||
clave?: string
|
codigo?: Asignatura['codigo']
|
||||||
tipo: TipoAsignatura
|
tipo: Asignatura['tipo'] | null
|
||||||
creditos: number
|
creditos: Asignatura['creditos'] | null
|
||||||
horasSemana?: number
|
horasAcademicas?: Asignatura['horas_academicas'] | null
|
||||||
estructuraId: UUID
|
horasIndependientes?: Asignatura['horas_independientes'] | null
|
||||||
|
estructuraId: Asignatura['estructura_id'] | null
|
||||||
}
|
}
|
||||||
iaConfig: {
|
// clonInterno?: {
|
||||||
descripcionEnfoque: string
|
// facultadId?: string
|
||||||
notasAdicionales?: string
|
// carreraId?: string
|
||||||
archivosExistentesIds?: Array<UUID>
|
// planOrigenId?: string
|
||||||
repositoriosIds?: Array<UUID>
|
// asignaturaOrigenId?: string | null
|
||||||
archivosAdhocIds?: Array<UUID>
|
// }
|
||||||
usarMCP?: boolean
|
// clonTradicional?: {
|
||||||
|
// archivoWordAsignaturaId: string | null
|
||||||
|
// archivosAdicionalesIds: Array<string>
|
||||||
|
// }
|
||||||
|
iaConfig?: {
|
||||||
|
descripcionEnfoqueAcademico: string
|
||||||
|
instruccionesAdicionalesIA: string
|
||||||
|
archivosReferencia: Array<string>
|
||||||
|
repositoriosReferencia?: Array<string>
|
||||||
|
archivosAdjuntos?: Array<UploadedFile>
|
||||||
}
|
}
|
||||||
}): Promise<any> {
|
}
|
||||||
return invokeEdge<any>(EDGE.ai_generate_subject, payload)
|
|
||||||
|
export async function ai_generate_subject(
|
||||||
|
input: AIGenerateSubjectInput,
|
||||||
|
): Promise<any> {
|
||||||
|
const edgeFunctionBody = new FormData()
|
||||||
|
edgeFunctionBody.append('plan_estudio_id', input.plan_estudio_id)
|
||||||
|
edgeFunctionBody.append('datosBasicos', JSON.stringify(input.datosBasicos))
|
||||||
|
edgeFunctionBody.append(
|
||||||
|
'iaConfig',
|
||||||
|
JSON.stringify({
|
||||||
|
...input.iaConfig,
|
||||||
|
archivosAdjuntos: undefined, // los manejamos aparte
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
input.iaConfig?.archivosAdjuntos?.forEach((file, index) => {
|
||||||
|
edgeFunctionBody.append(`archivosAdjuntos`, file.file)
|
||||||
|
})
|
||||||
|
return invokeEdge<any>(
|
||||||
|
EDGE.ai_generate_subject,
|
||||||
|
edgeFunctionBody,
|
||||||
|
undefined,
|
||||||
|
supabaseBrowser(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function subjects_persist_from_ai(payload: {
|
export async function subjects_persist_from_ai(payload: {
|
||||||
@@ -227,6 +261,23 @@ export async function subjects_get_document(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
export async function asignaturas_update(
|
export async function asignaturas_update(
|
||||||
asignaturaId: UUID,
|
asignaturaId: UUID,
|
||||||
patch: Partial<Asignatura>, // O tu tipo específico para el Patch de materias
|
patch: Partial<Asignatura>, // O tu tipo específico para el Patch de materias
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ export function usePlanDocumento(planId: UUID | null | undefined) {
|
|||||||
|
|
||||||
export function useCatalogosPlanes() {
|
export function useCatalogosPlanes() {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ['catalogos_planes'],
|
queryKey: qk.estructurasPlan(),
|
||||||
queryFn: getCatalogos,
|
queryFn: getCatalogos,
|
||||||
staleTime: 1000 * 60 * 60, // 1 hora de caché (estos datos casi no cambian)
|
staleTime: 1000 * 60 * 60, // 1 hora de caché (estos datos casi no cambian)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
subjects_generate_document,
|
subjects_generate_document,
|
||||||
subjects_get,
|
subjects_get,
|
||||||
subjects_get_document,
|
subjects_get_document,
|
||||||
|
subjects_get_structure_catalog,
|
||||||
subjects_history,
|
subjects_history,
|
||||||
subjects_import_from_file,
|
subjects_import_from_file,
|
||||||
subjects_persist_from_ai,
|
subjects_persist_from_ai,
|
||||||
@@ -68,6 +69,13 @@ export function useSubjectDocumento(subjectId: UUID | null | undefined) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useSubjectEstructuras() {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: qk.estructurasAsignatura(),
|
||||||
|
queryFn: () => subjects_get_structure_catalog(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/* ------------------ Mutations ------------------ */
|
/* ------------------ Mutations ------------------ */
|
||||||
|
|
||||||
export function useCreateSubjectManual() {
|
export function useCreateSubjectManual() {
|
||||||
@@ -89,7 +97,10 @@ export function useCreateSubjectManual() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useGenerateSubjectAI() {
|
export function useGenerateSubjectAI() {
|
||||||
return useMutation({ mutationFn: ai_generate_subject })
|
const qc = useQueryClient()
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: ai_generate_subject,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function usePersistSubjectFromAI() {
|
export function usePersistSubjectFromAI() {
|
||||||
|
|||||||
@@ -1,12 +1,18 @@
|
|||||||
import { supabaseBrowser } from "./client";
|
import {
|
||||||
|
FunctionsFetchError,
|
||||||
|
FunctionsHttpError,
|
||||||
|
FunctionsRelayError,
|
||||||
|
} from '@supabase/supabase-js'
|
||||||
|
|
||||||
import type { Database } from "@/types/supabase";
|
import { supabaseBrowser } from './client'
|
||||||
import type { SupabaseClient } from "@supabase/supabase-js";
|
|
||||||
|
import type { Database } from '@/types/supabase'
|
||||||
|
import type { SupabaseClient } from '@supabase/supabase-js'
|
||||||
|
|
||||||
export type EdgeInvokeOptions = {
|
export type EdgeInvokeOptions = {
|
||||||
method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'
|
||||||
headers?: Record<string, string>;
|
headers?: Record<string, string>
|
||||||
};
|
}
|
||||||
|
|
||||||
export class EdgeFunctionError extends Error {
|
export class EdgeFunctionError extends Error {
|
||||||
constructor(
|
constructor(
|
||||||
@@ -15,8 +21,8 @@ export class EdgeFunctionError extends Error {
|
|||||||
public readonly status?: number,
|
public readonly status?: number,
|
||||||
public readonly details?: unknown,
|
public readonly details?: unknown,
|
||||||
) {
|
) {
|
||||||
super(message);
|
super(message)
|
||||||
this.name = "EdgeFunctionError";
|
this.name = 'EdgeFunctionError'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,23 +40,69 @@ export async function invokeEdge<TOut>(
|
|||||||
opts: EdgeInvokeOptions = {},
|
opts: EdgeInvokeOptions = {},
|
||||||
client?: SupabaseClient<Database>,
|
client?: SupabaseClient<Database>,
|
||||||
): Promise<TOut> {
|
): Promise<TOut> {
|
||||||
const supabase = client ?? supabaseBrowser();
|
const supabase = client ?? supabaseBrowser()
|
||||||
|
|
||||||
const { data, error } = await supabase.functions.invoke(functionName, {
|
const { data, error } = await supabase.functions.invoke(functionName, {
|
||||||
body,
|
body,
|
||||||
method: opts.method ?? "POST",
|
method: opts.method ?? 'POST',
|
||||||
headers: opts.headers,
|
headers: opts.headers,
|
||||||
});
|
})
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
const anyErr = error;
|
// Valores por defecto (por si falla el parseo o es otro tipo de error)
|
||||||
throw new EdgeFunctionError(
|
let message = error.message // El genérico "returned a non-2xx status code"
|
||||||
anyErr.message ?? "Error en Edge Function",
|
let status = undefined
|
||||||
functionName,
|
let details: unknown = error
|
||||||
anyErr.status,
|
|
||||||
anyErr,
|
// 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 { useNuevaAsignaturaWizard } from './hooks/useNuevaAsignaturaWizard'
|
||||||
|
|
||||||
import { PasoBasicosForm } from '@/components/asignaturas/wizard/PasoBasicosForm'
|
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 { PasoMetodoCardGroup } from '@/components/asignaturas/wizard/PasoMetodoCardGroup'
|
||||||
import { PasoResumenCard } from '@/components/asignaturas/wizard/PasoResumenCard'
|
import { PasoResumenCard } from '@/components/asignaturas/wizard/PasoResumenCard'
|
||||||
import { WizardControls } from '@/components/asignaturas/wizard/WizardControls'
|
import { WizardControls } from '@/components/asignaturas/wizard/WizardControls'
|
||||||
@@ -54,7 +54,7 @@ export function NuevaAsignaturaModalContainer({ planId }: { planId: string }) {
|
|||||||
setWizard,
|
setWizard,
|
||||||
canContinueDesdeMetodo,
|
canContinueDesdeMetodo,
|
||||||
canContinueDesdeBasicos,
|
canContinueDesdeBasicos,
|
||||||
canContinueDesdeConfig,
|
canContinueDesdeDetalles,
|
||||||
simularGeneracionIA,
|
simularGeneracionIA,
|
||||||
crearAsignatura,
|
crearAsignatura,
|
||||||
} = useNuevaAsignaturaWizard(planId)
|
} = useNuevaAsignaturaWizard(planId)
|
||||||
@@ -104,13 +104,20 @@ export function NuevaAsignaturaModalContainer({ planId }: { planId: string }) {
|
|||||||
footerSlot={
|
footerSlot={
|
||||||
<Wizard.Stepper.Controls>
|
<Wizard.Stepper.Controls>
|
||||||
<WizardControls
|
<WizardControls
|
||||||
Wizard={Wizard}
|
errorMessage={wizard.errorMessage}
|
||||||
methods={methods}
|
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}
|
wizard={wizard}
|
||||||
canContinueDesdeMetodo={canContinueDesdeMetodo}
|
setWizard={setWizard}
|
||||||
canContinueDesdeBasicos={canContinueDesdeBasicos}
|
|
||||||
canContinueDesdeConfig={canContinueDesdeConfig}
|
|
||||||
onCreate={() => crearAsignatura(handleClose)}
|
|
||||||
/>
|
/>
|
||||||
</Wizard.Stepper.Controls>
|
</Wizard.Stepper.Controls>
|
||||||
}
|
}
|
||||||
@@ -130,7 +137,7 @@ export function NuevaAsignaturaModalContainer({ planId }: { planId: string }) {
|
|||||||
|
|
||||||
{idx === 2 && (
|
{idx === 2 && (
|
||||||
<Wizard.Stepper.Panel>
|
<Wizard.Stepper.Panel>
|
||||||
<PasoConfiguracionPanel
|
<PasoDetallesPanel
|
||||||
wizard={wizard}
|
wizard={wizard}
|
||||||
onChange={setWizard}
|
onChange={setWizard}
|
||||||
onGenerarIA={simularGeneracionIA}
|
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) {
|
export function useNuevaAsignaturaWizard(planId: string) {
|
||||||
const [wizard, setWizard] = useState<NewSubjectWizardState>({
|
const [wizard, setWizard] = useState<NewSubjectWizardState>({
|
||||||
step: 1,
|
step: 1,
|
||||||
planId,
|
plan_estudio_id: planId,
|
||||||
modoCreacion: null,
|
tipoOrigen: null,
|
||||||
datosBasicos: {
|
datosBasicos: {
|
||||||
nombre: "",
|
nombre: '',
|
||||||
clave: "",
|
codigo: '',
|
||||||
tipo: "OBLIGATORIA",
|
tipo: null,
|
||||||
creditos: 0,
|
creditos: null,
|
||||||
horasSemana: 0,
|
horasAcademicas: null,
|
||||||
estructuraId: "",
|
horasIndependientes: null,
|
||||||
|
estructuraId: '',
|
||||||
},
|
},
|
||||||
clonInterno: {},
|
clonInterno: {},
|
||||||
clonTradicional: {
|
clonTradicional: {
|
||||||
@@ -21,42 +22,47 @@ export function useNuevaAsignaturaWizard(planId: string) {
|
|||||||
archivosAdicionalesIds: [],
|
archivosAdicionalesIds: [],
|
||||||
},
|
},
|
||||||
iaConfig: {
|
iaConfig: {
|
||||||
descripcionEnfoque: "",
|
descripcionEnfoqueAcademico: '',
|
||||||
notasAdicionales: "",
|
instruccionesAdicionalesIA: '',
|
||||||
archivosExistentesIds: [],
|
archivosReferencia: [],
|
||||||
|
repositoriosReferencia: [],
|
||||||
|
archivosAdjuntos: [],
|
||||||
},
|
},
|
||||||
resumen: {},
|
resumen: {},
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
errorMessage: null,
|
errorMessage: null,
|
||||||
});
|
})
|
||||||
|
|
||||||
const canContinueDesdeMetodo = wizard.modoCreacion === "MANUAL" ||
|
const canContinueDesdeMetodo =
|
||||||
wizard.modoCreacion === "IA" ||
|
wizard.tipoOrigen === 'MANUAL' ||
|
||||||
(wizard.modoCreacion === "CLONADO" && !!wizard.subModoClonado);
|
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.creditos > 0 &&
|
||||||
!!wizard.datosBasicos.estructuraId;
|
!!wizard.datosBasicos.estructuraId
|
||||||
|
|
||||||
const canContinueDesdeConfig = (() => {
|
const canContinueDesdeDetalles = (() => {
|
||||||
if (wizard.modoCreacion === "MANUAL") return true;
|
if (wizard.tipoOrigen === 'MANUAL') return true
|
||||||
if (wizard.modoCreacion === "IA") {
|
if (wizard.tipoOrigen === 'IA') {
|
||||||
return !!wizard.iaConfig?.descripcionEnfoque;
|
return !!wizard.iaConfig?.descripcionEnfoqueAcademico
|
||||||
}
|
}
|
||||||
if (wizard.modoCreacion === "CLONADO") {
|
if (wizard.tipoOrigen === 'CLONADO_INTERNO') {
|
||||||
if (wizard.subModoClonado === "INTERNO") {
|
return !!wizard.clonInterno?.asignaturaOrigenId
|
||||||
return !!wizard.clonInterno?.asignaturaOrigenId;
|
|
||||||
}
|
}
|
||||||
if (wizard.subModoClonado === "TRADICIONAL") {
|
if (wizard.tipoOrigen === 'CLONADO_TRADICIONAL') {
|
||||||
return !!wizard.clonTradicional?.archivoWordAsignaturaId;
|
return !!wizard.clonTradicional?.archivoWordAsignaturaId
|
||||||
}
|
}
|
||||||
}
|
return false
|
||||||
return false;
|
})()
|
||||||
})();
|
|
||||||
|
|
||||||
const simularGeneracionIA = async () => {
|
const simularGeneracionIA = async () => {
|
||||||
setWizard((w) => ({ ...w, isLoading: true }));
|
setWizard((w) => ({ ...w, isLoading: true }))
|
||||||
await new Promise((r) => setTimeout(r, 1500));
|
await new Promise((r) => setTimeout(r, 1500))
|
||||||
setWizard((w) => ({
|
setWizard((w) => ({
|
||||||
...w,
|
...w,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
@@ -64,27 +70,25 @@ export function useNuevaAsignaturaWizard(planId: string) {
|
|||||||
previewAsignatura: {
|
previewAsignatura: {
|
||||||
nombre: w.datosBasicos.nombre,
|
nombre: w.datosBasicos.nombre,
|
||||||
objetivo:
|
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,
|
unidades: 5,
|
||||||
bibliografiaCount: 3,
|
bibliografiaCount: 3,
|
||||||
} as AsignaturaPreview,
|
} as AsignaturaPreview,
|
||||||
},
|
},
|
||||||
}));
|
}))
|
||||||
};
|
}
|
||||||
|
|
||||||
const crearAsignatura = async (onCreated: () => void) => {
|
const crearAsignatura = async () => {
|
||||||
setWizard((w) => ({ ...w, isLoading: true }));
|
await new Promise((r) => setTimeout(r, 1000))
|
||||||
await new Promise((r) => setTimeout(r, 1000));
|
}
|
||||||
onCreated();
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
wizard,
|
wizard,
|
||||||
setWizard,
|
setWizard,
|
||||||
canContinueDesdeMetodo,
|
canContinueDesdeMetodo,
|
||||||
canContinueDesdeBasicos,
|
canContinueDesdeBasicos,
|
||||||
canContinueDesdeConfig,
|
canContinueDesdeDetalles,
|
||||||
simularGeneracionIA,
|
simularGeneracionIA,
|
||||||
crearAsignatura,
|
crearAsignatura,
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,45 +1,50 @@
|
|||||||
export type ModoCreacion = "MANUAL" | "IA" | "CLONADO";
|
import type { UploadedFile } from '@/components/planes/wizard/PasoDetallesPanel/FileDropZone'
|
||||||
export type SubModoClonado = "INTERNO" | "TRADICIONAL";
|
import type { Asignatura } from '@/data'
|
||||||
export type TipoAsignatura = "OBLIGATORIA" | "OPTATIVA" | "TRONCAL" | "OTRO";
|
|
||||||
|
export type ModoCreacion = 'MANUAL' | 'IA' | 'CLONADO'
|
||||||
|
export type SubModoClonado = 'INTERNO' | 'TRADICIONAL'
|
||||||
|
export type TipoAsignatura = 'OBLIGATORIA' | 'OPTATIVA' | 'TRONCAL' | 'OTRO'
|
||||||
|
|
||||||
export type AsignaturaPreview = {
|
export type AsignaturaPreview = {
|
||||||
nombre: string;
|
nombre: string
|
||||||
objetivo: string;
|
objetivo: string
|
||||||
unidades: number;
|
unidades: number
|
||||||
bibliografiaCount: number;
|
bibliografiaCount: number
|
||||||
};
|
}
|
||||||
|
|
||||||
export type NewSubjectWizardState = {
|
export type NewSubjectWizardState = {
|
||||||
step: 1 | 2 | 3 | 4;
|
step: 1 | 2 | 3 | 4
|
||||||
planId: string;
|
plan_estudio_id: Asignatura['plan_estudio_id']
|
||||||
modoCreacion: ModoCreacion | null;
|
tipoOrigen: Asignatura['tipo_origen'] | null
|
||||||
subModoClonado?: SubModoClonado;
|
|
||||||
datosBasicos: {
|
datosBasicos: {
|
||||||
nombre: string;
|
nombre: Asignatura['nombre']
|
||||||
clave?: string;
|
codigo?: Asignatura['codigo']
|
||||||
tipo: TipoAsignatura;
|
tipo: Asignatura['tipo'] | null
|
||||||
creditos: number;
|
creditos: Asignatura['creditos'] | null
|
||||||
horasSemana?: number;
|
horasAcademicas?: Asignatura['horas_academicas'] | null
|
||||||
estructuraId: string;
|
horasIndependientes?: Asignatura['horas_independientes'] | null
|
||||||
};
|
estructuraId: Asignatura['estructura_id'] | null
|
||||||
|
}
|
||||||
clonInterno?: {
|
clonInterno?: {
|
||||||
facultadId?: string;
|
facultadId?: string
|
||||||
carreraId?: string;
|
carreraId?: string
|
||||||
planOrigenId?: string;
|
planOrigenId?: string
|
||||||
asignaturaOrigenId?: string | null;
|
asignaturaOrigenId?: string | null
|
||||||
};
|
}
|
||||||
clonTradicional?: {
|
clonTradicional?: {
|
||||||
archivoWordAsignaturaId: string | null;
|
archivoWordAsignaturaId: string | null
|
||||||
archivosAdicionalesIds: Array<string>;
|
archivosAdicionalesIds: Array<string>
|
||||||
};
|
}
|
||||||
iaConfig?: {
|
iaConfig?: {
|
||||||
descripcionEnfoque: string;
|
descripcionEnfoqueAcademico: string
|
||||||
notasAdicionales: string;
|
instruccionesAdicionalesIA: string
|
||||||
archivosExistentesIds: Array<string>;
|
archivosReferencia: Array<string>
|
||||||
};
|
repositoriosReferencia?: Array<string>
|
||||||
|
archivosAdjuntos?: Array<UploadedFile>
|
||||||
|
}
|
||||||
resumen: {
|
resumen: {
|
||||||
previewAsignatura?: AsignaturaPreview;
|
previewAsignatura?: AsignaturaPreview
|
||||||
};
|
}
|
||||||
isLoading: boolean;
|
isLoading: boolean
|
||||||
errorMessage: string | null;
|
errorMessage: string | null
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export function useNuevoPlanWizard() {
|
|||||||
carrera: { id: '', nombre: '' },
|
carrera: { id: '', nombre: '' },
|
||||||
nivel: '',
|
nivel: '',
|
||||||
tipoCiclo: '',
|
tipoCiclo: '',
|
||||||
numCiclos: undefined,
|
numCiclos: null,
|
||||||
estructuraPlanId: null,
|
estructuraPlanId: null,
|
||||||
},
|
},
|
||||||
// datosBasicos: {
|
// datosBasicos: {
|
||||||
@@ -56,7 +56,7 @@ export function useNuevoPlanWizard() {
|
|||||||
!!wizard.datosBasicos.carrera.id &&
|
!!wizard.datosBasicos.carrera.id &&
|
||||||
!!wizard.datosBasicos.facultad.id &&
|
!!wizard.datosBasicos.facultad.id &&
|
||||||
!!wizard.datosBasicos.nivel &&
|
!!wizard.datosBasicos.nivel &&
|
||||||
wizard.datosBasicos.numCiclos !== undefined &&
|
wizard.datosBasicos.numCiclos !== null &&
|
||||||
wizard.datosBasicos.numCiclos > 0 &&
|
wizard.datosBasicos.numCiclos > 0 &&
|
||||||
// Requerir ambas plantillas (plan y mapa) con versión
|
// Requerir ambas plantillas (plan y mapa) con versión
|
||||||
!!wizard.datosBasicos.estructuraPlanId
|
!!wizard.datosBasicos.estructuraPlanId
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export type NewPlanWizardState = {
|
|||||||
}
|
}
|
||||||
nivel: NivelPlanEstudio | ''
|
nivel: NivelPlanEstudio | ''
|
||||||
tipoCiclo: TipoCiclo | ''
|
tipoCiclo: TipoCiclo | ''
|
||||||
numCiclos: number | undefined
|
numCiclos: number | null
|
||||||
// Selección de plantillas (obligatorias)
|
// Selección de plantillas (obligatorias)
|
||||||
estructuraPlanId: string | null
|
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 PlanesPlanIdDetalleHistorialRouteImport } from './routes/planes/$planId/_detalle/historial'
|
||||||
import { Route as PlanesPlanIdDetalleFlujoRouteImport } from './routes/planes/$planId/_detalle/flujo'
|
import { Route as PlanesPlanIdDetalleFlujoRouteImport } from './routes/planes/$planId/_detalle/flujo'
|
||||||
import { Route as PlanesPlanIdDetalleDocumentoRouteImport } from './routes/planes/$planId/_detalle/documento'
|
import { Route as PlanesPlanIdDetalleDocumentoRouteImport } from './routes/planes/$planId/_detalle/documento'
|
||||||
import { Route as 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'
|
import { Route as PlanesPlanIdDetalleAsignaturasNuevaRouteImport } from './routes/planes/$planId/_detalle/asignaturas/nueva'
|
||||||
|
|
||||||
const LoginRoute = LoginRouteImport.update({
|
const LoginRoute = LoginRouteImport.update({
|
||||||
@@ -102,17 +102,17 @@ const PlanesPlanIdDetalleDocumentoRoute =
|
|||||||
path: '/documento',
|
path: '/documento',
|
||||||
getParentRoute: () => PlanesPlanIdDetalleRoute,
|
getParentRoute: () => PlanesPlanIdDetalleRoute,
|
||||||
} as any)
|
} as any)
|
||||||
const PlanesPlanIdDetalleAsignaturasIndexRoute =
|
const PlanesPlanIdDetalleAsignaturasRoute =
|
||||||
PlanesPlanIdDetalleAsignaturasIndexRouteImport.update({
|
PlanesPlanIdDetalleAsignaturasRouteImport.update({
|
||||||
id: '/asignaturas/',
|
id: '/asignaturas',
|
||||||
path: '/asignaturas/',
|
path: '/asignaturas',
|
||||||
getParentRoute: () => PlanesPlanIdDetalleRoute,
|
getParentRoute: () => PlanesPlanIdDetalleRoute,
|
||||||
} as any)
|
} as any)
|
||||||
const PlanesPlanIdDetalleAsignaturasNuevaRoute =
|
const PlanesPlanIdDetalleAsignaturasNuevaRoute =
|
||||||
PlanesPlanIdDetalleAsignaturasNuevaRouteImport.update({
|
PlanesPlanIdDetalleAsignaturasNuevaRouteImport.update({
|
||||||
id: '/asignaturas/nueva',
|
id: '/nueva',
|
||||||
path: '/asignaturas/nueva',
|
path: '/nueva',
|
||||||
getParentRoute: () => PlanesPlanIdDetalleRoute,
|
getParentRoute: () => PlanesPlanIdDetalleAsignaturasRoute,
|
||||||
} as any)
|
} as any)
|
||||||
|
|
||||||
export interface FileRoutesByFullPath {
|
export interface FileRoutesByFullPath {
|
||||||
@@ -123,6 +123,7 @@ export interface FileRoutesByFullPath {
|
|||||||
'/planes': typeof PlanesListaRouteWithChildren
|
'/planes': typeof PlanesListaRouteWithChildren
|
||||||
'/planes/$planId': typeof PlanesPlanIdDetalleRouteWithChildren
|
'/planes/$planId': typeof PlanesPlanIdDetalleRouteWithChildren
|
||||||
'/planes/nuevo': typeof PlanesListaNuevoRoute
|
'/planes/nuevo': typeof PlanesListaNuevoRoute
|
||||||
|
'/planes/$planId/asignaturas': typeof PlanesPlanIdDetalleAsignaturasRouteWithChildren
|
||||||
'/planes/$planId/documento': typeof PlanesPlanIdDetalleDocumentoRoute
|
'/planes/$planId/documento': typeof PlanesPlanIdDetalleDocumentoRoute
|
||||||
'/planes/$planId/flujo': typeof PlanesPlanIdDetalleFlujoRoute
|
'/planes/$planId/flujo': typeof PlanesPlanIdDetalleFlujoRoute
|
||||||
'/planes/$planId/historial': typeof PlanesPlanIdDetalleHistorialRoute
|
'/planes/$planId/historial': typeof PlanesPlanIdDetalleHistorialRoute
|
||||||
@@ -131,7 +132,6 @@ export interface FileRoutesByFullPath {
|
|||||||
'/planes/$planId/asignaturas/$asignaturaId': typeof PlanesPlanIdAsignaturasAsignaturaIdRoute
|
'/planes/$planId/asignaturas/$asignaturaId': typeof PlanesPlanIdAsignaturasAsignaturaIdRoute
|
||||||
'/planes/$planId/': typeof PlanesPlanIdDetalleIndexRoute
|
'/planes/$planId/': typeof PlanesPlanIdDetalleIndexRoute
|
||||||
'/planes/$planId/asignaturas/nueva': typeof PlanesPlanIdDetalleAsignaturasNuevaRoute
|
'/planes/$planId/asignaturas/nueva': typeof PlanesPlanIdDetalleAsignaturasNuevaRoute
|
||||||
'/planes/$planId/asignaturas/': typeof PlanesPlanIdDetalleAsignaturasIndexRoute
|
|
||||||
}
|
}
|
||||||
export interface FileRoutesByTo {
|
export interface FileRoutesByTo {
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
@@ -140,6 +140,7 @@ export interface FileRoutesByTo {
|
|||||||
'/demo/tanstack-query': typeof DemoTanstackQueryRoute
|
'/demo/tanstack-query': typeof DemoTanstackQueryRoute
|
||||||
'/planes': typeof PlanesListaRouteWithChildren
|
'/planes': typeof PlanesListaRouteWithChildren
|
||||||
'/planes/nuevo': typeof PlanesListaNuevoRoute
|
'/planes/nuevo': typeof PlanesListaNuevoRoute
|
||||||
|
'/planes/$planId/asignaturas': typeof PlanesPlanIdDetalleAsignaturasRouteWithChildren
|
||||||
'/planes/$planId/documento': typeof PlanesPlanIdDetalleDocumentoRoute
|
'/planes/$planId/documento': typeof PlanesPlanIdDetalleDocumentoRoute
|
||||||
'/planes/$planId/flujo': typeof PlanesPlanIdDetalleFlujoRoute
|
'/planes/$planId/flujo': typeof PlanesPlanIdDetalleFlujoRoute
|
||||||
'/planes/$planId/historial': typeof PlanesPlanIdDetalleHistorialRoute
|
'/planes/$planId/historial': typeof PlanesPlanIdDetalleHistorialRoute
|
||||||
@@ -148,7 +149,6 @@ export interface FileRoutesByTo {
|
|||||||
'/planes/$planId/asignaturas/$asignaturaId': typeof PlanesPlanIdAsignaturasAsignaturaIdRoute
|
'/planes/$planId/asignaturas/$asignaturaId': typeof PlanesPlanIdAsignaturasAsignaturaIdRoute
|
||||||
'/planes/$planId': typeof PlanesPlanIdDetalleIndexRoute
|
'/planes/$planId': typeof PlanesPlanIdDetalleIndexRoute
|
||||||
'/planes/$planId/asignaturas/nueva': typeof PlanesPlanIdDetalleAsignaturasNuevaRoute
|
'/planes/$planId/asignaturas/nueva': typeof PlanesPlanIdDetalleAsignaturasNuevaRoute
|
||||||
'/planes/$planId/asignaturas': typeof PlanesPlanIdDetalleAsignaturasIndexRoute
|
|
||||||
}
|
}
|
||||||
export interface FileRoutesById {
|
export interface FileRoutesById {
|
||||||
__root__: typeof rootRouteImport
|
__root__: typeof rootRouteImport
|
||||||
@@ -159,6 +159,7 @@ export interface FileRoutesById {
|
|||||||
'/planes/_lista': typeof PlanesListaRouteWithChildren
|
'/planes/_lista': typeof PlanesListaRouteWithChildren
|
||||||
'/planes/$planId/_detalle': typeof PlanesPlanIdDetalleRouteWithChildren
|
'/planes/$planId/_detalle': typeof PlanesPlanIdDetalleRouteWithChildren
|
||||||
'/planes/_lista/nuevo': typeof PlanesListaNuevoRoute
|
'/planes/_lista/nuevo': typeof PlanesListaNuevoRoute
|
||||||
|
'/planes/$planId/_detalle/asignaturas': typeof PlanesPlanIdDetalleAsignaturasRouteWithChildren
|
||||||
'/planes/$planId/_detalle/documento': typeof PlanesPlanIdDetalleDocumentoRoute
|
'/planes/$planId/_detalle/documento': typeof PlanesPlanIdDetalleDocumentoRoute
|
||||||
'/planes/$planId/_detalle/flujo': typeof PlanesPlanIdDetalleFlujoRoute
|
'/planes/$planId/_detalle/flujo': typeof PlanesPlanIdDetalleFlujoRoute
|
||||||
'/planes/$planId/_detalle/historial': typeof PlanesPlanIdDetalleHistorialRoute
|
'/planes/$planId/_detalle/historial': typeof PlanesPlanIdDetalleHistorialRoute
|
||||||
@@ -167,7 +168,6 @@ export interface FileRoutesById {
|
|||||||
'/planes/$planId/asignaturas/$asignaturaId': typeof PlanesPlanIdAsignaturasAsignaturaIdRoute
|
'/planes/$planId/asignaturas/$asignaturaId': typeof PlanesPlanIdAsignaturasAsignaturaIdRoute
|
||||||
'/planes/$planId/_detalle/': typeof PlanesPlanIdDetalleIndexRoute
|
'/planes/$planId/_detalle/': typeof PlanesPlanIdDetalleIndexRoute
|
||||||
'/planes/$planId/_detalle/asignaturas/nueva': typeof PlanesPlanIdDetalleAsignaturasNuevaRoute
|
'/planes/$planId/_detalle/asignaturas/nueva': typeof PlanesPlanIdDetalleAsignaturasNuevaRoute
|
||||||
'/planes/$planId/_detalle/asignaturas/': typeof PlanesPlanIdDetalleAsignaturasIndexRoute
|
|
||||||
}
|
}
|
||||||
export interface FileRouteTypes {
|
export interface FileRouteTypes {
|
||||||
fileRoutesByFullPath: FileRoutesByFullPath
|
fileRoutesByFullPath: FileRoutesByFullPath
|
||||||
@@ -179,6 +179,7 @@ export interface FileRouteTypes {
|
|||||||
| '/planes'
|
| '/planes'
|
||||||
| '/planes/$planId'
|
| '/planes/$planId'
|
||||||
| '/planes/nuevo'
|
| '/planes/nuevo'
|
||||||
|
| '/planes/$planId/asignaturas'
|
||||||
| '/planes/$planId/documento'
|
| '/planes/$planId/documento'
|
||||||
| '/planes/$planId/flujo'
|
| '/planes/$planId/flujo'
|
||||||
| '/planes/$planId/historial'
|
| '/planes/$planId/historial'
|
||||||
@@ -187,7 +188,6 @@ export interface FileRouteTypes {
|
|||||||
| '/planes/$planId/asignaturas/$asignaturaId'
|
| '/planes/$planId/asignaturas/$asignaturaId'
|
||||||
| '/planes/$planId/'
|
| '/planes/$planId/'
|
||||||
| '/planes/$planId/asignaturas/nueva'
|
| '/planes/$planId/asignaturas/nueva'
|
||||||
| '/planes/$planId/asignaturas/'
|
|
||||||
fileRoutesByTo: FileRoutesByTo
|
fileRoutesByTo: FileRoutesByTo
|
||||||
to:
|
to:
|
||||||
| '/'
|
| '/'
|
||||||
@@ -196,6 +196,7 @@ export interface FileRouteTypes {
|
|||||||
| '/demo/tanstack-query'
|
| '/demo/tanstack-query'
|
||||||
| '/planes'
|
| '/planes'
|
||||||
| '/planes/nuevo'
|
| '/planes/nuevo'
|
||||||
|
| '/planes/$planId/asignaturas'
|
||||||
| '/planes/$planId/documento'
|
| '/planes/$planId/documento'
|
||||||
| '/planes/$planId/flujo'
|
| '/planes/$planId/flujo'
|
||||||
| '/planes/$planId/historial'
|
| '/planes/$planId/historial'
|
||||||
@@ -204,7 +205,6 @@ export interface FileRouteTypes {
|
|||||||
| '/planes/$planId/asignaturas/$asignaturaId'
|
| '/planes/$planId/asignaturas/$asignaturaId'
|
||||||
| '/planes/$planId'
|
| '/planes/$planId'
|
||||||
| '/planes/$planId/asignaturas/nueva'
|
| '/planes/$planId/asignaturas/nueva'
|
||||||
| '/planes/$planId/asignaturas'
|
|
||||||
id:
|
id:
|
||||||
| '__root__'
|
| '__root__'
|
||||||
| '/'
|
| '/'
|
||||||
@@ -214,6 +214,7 @@ export interface FileRouteTypes {
|
|||||||
| '/planes/_lista'
|
| '/planes/_lista'
|
||||||
| '/planes/$planId/_detalle'
|
| '/planes/$planId/_detalle'
|
||||||
| '/planes/_lista/nuevo'
|
| '/planes/_lista/nuevo'
|
||||||
|
| '/planes/$planId/_detalle/asignaturas'
|
||||||
| '/planes/$planId/_detalle/documento'
|
| '/planes/$planId/_detalle/documento'
|
||||||
| '/planes/$planId/_detalle/flujo'
|
| '/planes/$planId/_detalle/flujo'
|
||||||
| '/planes/$planId/_detalle/historial'
|
| '/planes/$planId/_detalle/historial'
|
||||||
@@ -222,7 +223,6 @@ export interface FileRouteTypes {
|
|||||||
| '/planes/$planId/asignaturas/$asignaturaId'
|
| '/planes/$planId/asignaturas/$asignaturaId'
|
||||||
| '/planes/$planId/_detalle/'
|
| '/planes/$planId/_detalle/'
|
||||||
| '/planes/$planId/_detalle/asignaturas/nueva'
|
| '/planes/$planId/_detalle/asignaturas/nueva'
|
||||||
| '/planes/$planId/_detalle/asignaturas/'
|
|
||||||
fileRoutesById: FileRoutesById
|
fileRoutesById: FileRoutesById
|
||||||
}
|
}
|
||||||
export interface RootRouteChildren {
|
export interface RootRouteChildren {
|
||||||
@@ -335,19 +335,19 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof PlanesPlanIdDetalleDocumentoRouteImport
|
preLoaderRoute: typeof PlanesPlanIdDetalleDocumentoRouteImport
|
||||||
parentRoute: typeof PlanesPlanIdDetalleRoute
|
parentRoute: typeof PlanesPlanIdDetalleRoute
|
||||||
}
|
}
|
||||||
'/planes/$planId/_detalle/asignaturas/': {
|
'/planes/$planId/_detalle/asignaturas': {
|
||||||
id: '/planes/$planId/_detalle/asignaturas/'
|
id: '/planes/$planId/_detalle/asignaturas'
|
||||||
path: '/asignaturas'
|
path: '/asignaturas'
|
||||||
fullPath: '/planes/$planId/asignaturas/'
|
fullPath: '/planes/$planId/asignaturas'
|
||||||
preLoaderRoute: typeof PlanesPlanIdDetalleAsignaturasIndexRouteImport
|
preLoaderRoute: typeof PlanesPlanIdDetalleAsignaturasRouteImport
|
||||||
parentRoute: typeof PlanesPlanIdDetalleRoute
|
parentRoute: typeof PlanesPlanIdDetalleRoute
|
||||||
}
|
}
|
||||||
'/planes/$planId/_detalle/asignaturas/nueva': {
|
'/planes/$planId/_detalle/asignaturas/nueva': {
|
||||||
id: '/planes/$planId/_detalle/asignaturas/nueva'
|
id: '/planes/$planId/_detalle/asignaturas/nueva'
|
||||||
path: '/asignaturas/nueva'
|
path: '/nueva'
|
||||||
fullPath: '/planes/$planId/asignaturas/nueva'
|
fullPath: '/planes/$planId/asignaturas/nueva'
|
||||||
preLoaderRoute: typeof PlanesPlanIdDetalleAsignaturasNuevaRouteImport
|
preLoaderRoute: typeof PlanesPlanIdDetalleAsignaturasNuevaRouteImport
|
||||||
parentRoute: typeof PlanesPlanIdDetalleRoute
|
parentRoute: typeof PlanesPlanIdDetalleAsignaturasRoute
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -364,28 +364,40 @@ const PlanesListaRouteWithChildren = PlanesListaRoute._addFileChildren(
|
|||||||
PlanesListaRouteChildren,
|
PlanesListaRouteChildren,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
interface PlanesPlanIdDetalleAsignaturasRouteChildren {
|
||||||
|
PlanesPlanIdDetalleAsignaturasNuevaRoute: typeof PlanesPlanIdDetalleAsignaturasNuevaRoute
|
||||||
|
}
|
||||||
|
|
||||||
|
const PlanesPlanIdDetalleAsignaturasRouteChildren: PlanesPlanIdDetalleAsignaturasRouteChildren =
|
||||||
|
{
|
||||||
|
PlanesPlanIdDetalleAsignaturasNuevaRoute:
|
||||||
|
PlanesPlanIdDetalleAsignaturasNuevaRoute,
|
||||||
|
}
|
||||||
|
|
||||||
|
const PlanesPlanIdDetalleAsignaturasRouteWithChildren =
|
||||||
|
PlanesPlanIdDetalleAsignaturasRoute._addFileChildren(
|
||||||
|
PlanesPlanIdDetalleAsignaturasRouteChildren,
|
||||||
|
)
|
||||||
|
|
||||||
interface PlanesPlanIdDetalleRouteChildren {
|
interface PlanesPlanIdDetalleRouteChildren {
|
||||||
|
PlanesPlanIdDetalleAsignaturasRoute: typeof PlanesPlanIdDetalleAsignaturasRouteWithChildren
|
||||||
PlanesPlanIdDetalleDocumentoRoute: typeof PlanesPlanIdDetalleDocumentoRoute
|
PlanesPlanIdDetalleDocumentoRoute: typeof PlanesPlanIdDetalleDocumentoRoute
|
||||||
PlanesPlanIdDetalleFlujoRoute: typeof PlanesPlanIdDetalleFlujoRoute
|
PlanesPlanIdDetalleFlujoRoute: typeof PlanesPlanIdDetalleFlujoRoute
|
||||||
PlanesPlanIdDetalleHistorialRoute: typeof PlanesPlanIdDetalleHistorialRoute
|
PlanesPlanIdDetalleHistorialRoute: typeof PlanesPlanIdDetalleHistorialRoute
|
||||||
PlanesPlanIdDetalleIaplanRoute: typeof PlanesPlanIdDetalleIaplanRoute
|
PlanesPlanIdDetalleIaplanRoute: typeof PlanesPlanIdDetalleIaplanRoute
|
||||||
PlanesPlanIdDetalleMapaRoute: typeof PlanesPlanIdDetalleMapaRoute
|
PlanesPlanIdDetalleMapaRoute: typeof PlanesPlanIdDetalleMapaRoute
|
||||||
PlanesPlanIdDetalleIndexRoute: typeof PlanesPlanIdDetalleIndexRoute
|
PlanesPlanIdDetalleIndexRoute: typeof PlanesPlanIdDetalleIndexRoute
|
||||||
PlanesPlanIdDetalleAsignaturasNuevaRoute: typeof PlanesPlanIdDetalleAsignaturasNuevaRoute
|
|
||||||
PlanesPlanIdDetalleAsignaturasIndexRoute: typeof PlanesPlanIdDetalleAsignaturasIndexRoute
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const PlanesPlanIdDetalleRouteChildren: PlanesPlanIdDetalleRouteChildren = {
|
const PlanesPlanIdDetalleRouteChildren: PlanesPlanIdDetalleRouteChildren = {
|
||||||
|
PlanesPlanIdDetalleAsignaturasRoute:
|
||||||
|
PlanesPlanIdDetalleAsignaturasRouteWithChildren,
|
||||||
PlanesPlanIdDetalleDocumentoRoute: PlanesPlanIdDetalleDocumentoRoute,
|
PlanesPlanIdDetalleDocumentoRoute: PlanesPlanIdDetalleDocumentoRoute,
|
||||||
PlanesPlanIdDetalleFlujoRoute: PlanesPlanIdDetalleFlujoRoute,
|
PlanesPlanIdDetalleFlujoRoute: PlanesPlanIdDetalleFlujoRoute,
|
||||||
PlanesPlanIdDetalleHistorialRoute: PlanesPlanIdDetalleHistorialRoute,
|
PlanesPlanIdDetalleHistorialRoute: PlanesPlanIdDetalleHistorialRoute,
|
||||||
PlanesPlanIdDetalleIaplanRoute: PlanesPlanIdDetalleIaplanRoute,
|
PlanesPlanIdDetalleIaplanRoute: PlanesPlanIdDetalleIaplanRoute,
|
||||||
PlanesPlanIdDetalleMapaRoute: PlanesPlanIdDetalleMapaRoute,
|
PlanesPlanIdDetalleMapaRoute: PlanesPlanIdDetalleMapaRoute,
|
||||||
PlanesPlanIdDetalleIndexRoute: PlanesPlanIdDetalleIndexRoute,
|
PlanesPlanIdDetalleIndexRoute: PlanesPlanIdDetalleIndexRoute,
|
||||||
PlanesPlanIdDetalleAsignaturasNuevaRoute:
|
|
||||||
PlanesPlanIdDetalleAsignaturasNuevaRoute,
|
|
||||||
PlanesPlanIdDetalleAsignaturasIndexRoute:
|
|
||||||
PlanesPlanIdDetalleAsignaturasIndexRoute,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const PlanesPlanIdDetalleRouteWithChildren =
|
const PlanesPlanIdDetalleRouteWithChildren =
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { createFileRoute, useNavigate } from '@tanstack/react-router'
|
import { createFileRoute, Outlet, useNavigate } from '@tanstack/react-router'
|
||||||
import {
|
import {
|
||||||
Plus,
|
Plus,
|
||||||
Copy,
|
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,
|
component: AsignaturasPage,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -306,6 +306,7 @@ function AsignaturasPage() {
|
|||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
|
<Outlet />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
import { createFileRoute, notFound } from '@tanstack/react-router'
|
import { createFileRoute, notFound, useLocation } from '@tanstack/react-router'
|
||||||
|
import { useEffect } from 'react'
|
||||||
|
|
||||||
import AsignaturaDetailPage from '@/components/asignaturas/detalle/AsignaturaDetailPage'
|
import AsignaturaDetailPage from '@/components/asignaturas/detalle/AsignaturaDetailPage'
|
||||||
|
import { lateralConfetti } from '@/components/ui/lateral-confetti'
|
||||||
import { NotFoundPage } from '@/components/ui/NotFoundPage'
|
import { NotFoundPage } from '@/components/ui/NotFoundPage'
|
||||||
import { subjects_get } from '@/data/api/subjects.api'
|
import { subjects_get } from '@/data/api/subjects.api'
|
||||||
import { qk } from '@/data/query/keys'
|
import { qk } from '@/data/query/keys'
|
||||||
@@ -35,6 +37,15 @@ export const Route = createFileRoute(
|
|||||||
|
|
||||||
function RouteComponent() {
|
function RouteComponent() {
|
||||||
// const { planId, asignaturaId } = Route.useParams()
|
// const { planId, asignaturaId } = Route.useParams()
|
||||||
|
const location = useLocation()
|
||||||
|
|
||||||
|
// Confetti al llegar desde creación
|
||||||
|
useEffect(() => {
|
||||||
|
if ((location.state as any)?.showConfetti) {
|
||||||
|
lateralConfetti()
|
||||||
|
window.history.replaceState({}, document.title) // Limpiar el estado para que no se repita
|
||||||
|
}
|
||||||
|
}, [location.state])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
Reference in New Issue
Block a user