Merge pull request 'Se agregan indicadores de que está generandose el plan o la asignatura' (#115) from issue/29-botn-de-generando-plan-de-estudios into main
Reviewed-on: #115
This commit was merged in pull request #115.
This commit is contained in:
@@ -1,12 +1,19 @@
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import { useNavigate } from '@tanstack/react-router'
|
||||
import { Loader2 } from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
|
||||
import type { AIGenerateSubjectInput, AIGenerateSubjectJsonInput } from '@/data'
|
||||
import type { NewSubjectWizardState } from '@/features/asignaturas/nueva/types'
|
||||
import type { TablesInsert } from '@/types/supabase'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { supabaseBrowser, useGenerateSubjectAI, qk } from '@/data'
|
||||
import {
|
||||
supabaseBrowser,
|
||||
useGenerateSubjectAI,
|
||||
qk,
|
||||
useCreateSubjectManual,
|
||||
} from '@/data'
|
||||
|
||||
export function WizardControls({
|
||||
wizard,
|
||||
@@ -32,6 +39,8 @@ export function WizardControls({
|
||||
const navigate = useNavigate()
|
||||
const qc = useQueryClient()
|
||||
const generateSubjectAI = useGenerateSubjectAI()
|
||||
const createSubjectManual = useCreateSubjectManual()
|
||||
const [isSpinningIA, setIsSpinningIA] = useState(false)
|
||||
const handleCreate = async () => {
|
||||
setWizard((w) => ({
|
||||
...w,
|
||||
@@ -40,7 +49,7 @@ export function WizardControls({
|
||||
}))
|
||||
|
||||
try {
|
||||
if (wizard.tipoOrigen === 'IA' || wizard.tipoOrigen === 'IA_SIMPLE') {
|
||||
if (wizard.tipoOrigen === 'IA_SIMPLE') {
|
||||
const aiInput: AIGenerateSubjectInput = {
|
||||
plan_estudio_id: wizard.plan_estudio_id,
|
||||
datosBasicos: {
|
||||
@@ -68,11 +77,14 @@ export function WizardControls({
|
||||
`${new Date().toISOString()} - Enviando a generar asignatura con IA`,
|
||||
)
|
||||
|
||||
setIsSpinningIA(true)
|
||||
const asignatura = await generateSubjectAI.mutateAsync(aiInput)
|
||||
console.log(
|
||||
`${new Date().toISOString()} - Asignatura IA generada`,
|
||||
asignatura,
|
||||
)
|
||||
// await new Promise((resolve) => setTimeout(resolve, 20000)) // debug
|
||||
setIsSpinningIA(false)
|
||||
// console.log(
|
||||
// `${new Date().toISOString()} - Asignatura IA generada`,
|
||||
// asignatura,
|
||||
// )
|
||||
|
||||
navigate({
|
||||
to: `/planes/${wizard.plan_estudio_id}/asignaturas/${asignatura.id}`,
|
||||
@@ -156,13 +168,40 @@ export function WizardControls({
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (wizard.tipoOrigen === 'MANUAL') {
|
||||
if (!wizard.plan_estudio_id) {
|
||||
throw new Error('Plan de estudio inválido.')
|
||||
}
|
||||
|
||||
const asignatura = await createSubjectManual.mutateAsync({
|
||||
plan_estudio_id: wizard.plan_estudio_id,
|
||||
estructura_id: wizard.datosBasicos.estructuraId!,
|
||||
nombre: wizard.datosBasicos.nombre,
|
||||
codigo: wizard.datosBasicos.codigo ?? null,
|
||||
tipo: wizard.datosBasicos.tipo ?? undefined,
|
||||
creditos: wizard.datosBasicos.creditos ?? 0,
|
||||
horas_academicas: wizard.datosBasicos.horasAcademicas ?? null,
|
||||
horas_independientes: wizard.datosBasicos.horasIndependientes ?? null,
|
||||
linea_plan_id: null,
|
||||
numero_ciclo: null,
|
||||
})
|
||||
|
||||
navigate({
|
||||
to: `/planes/${wizard.plan_estudio_id}/asignaturas/${asignatura.id}`,
|
||||
state: { showConfetti: true },
|
||||
resetScroll: false,
|
||||
})
|
||||
}
|
||||
} catch (err: any) {
|
||||
setIsSpinningIA(false)
|
||||
setWizard((w) => ({
|
||||
...w,
|
||||
isLoading: false,
|
||||
errorMessage: err?.message ?? 'Error creando la asignatura',
|
||||
}))
|
||||
} finally {
|
||||
setIsSpinningIA(false)
|
||||
setWizard((w) => ({ ...w, isLoading: false }))
|
||||
}
|
||||
}
|
||||
@@ -181,6 +220,17 @@ export function WizardControls({
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mx-2 flex w-5 items-center justify-center">
|
||||
<Loader2
|
||||
className={
|
||||
wizard.tipoOrigen === 'IA_SIMPLE' && isSpinningIA
|
||||
? 'text-muted-foreground h-6 w-6 animate-spin'
|
||||
: 'h-6 w-6 opacity-0'
|
||||
}
|
||||
aria-hidden={!(wizard.tipoOrigen === 'IA_SIMPLE' && isSpinningIA)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{isLastStep ? (
|
||||
<Button onClick={handleCreate} disabled={disableCreate}>
|
||||
{wizard.isLoading ? 'Creando...' : 'Crear Asignatura'}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { useNavigate } from '@tanstack/react-router'
|
||||
import { Loader2 } from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
|
||||
import type { AIGeneratePlanInput } from '@/data'
|
||||
import type { NivelPlanEstudio, TipoCiclo } from '@/data/types/domain'
|
||||
@@ -33,6 +35,7 @@ export function WizardControls({
|
||||
const navigate = useNavigate()
|
||||
const generatePlanAI = useGeneratePlanAI()
|
||||
const createPlanManual = useCreatePlanManual()
|
||||
const [isSpinningIA, setIsSpinningIA] = useState(false)
|
||||
// const supabaseClient = supabaseBrowser()
|
||||
// const persistPlanFromAI = usePersistPlanFromAI()
|
||||
|
||||
@@ -78,7 +81,9 @@ export function WizardControls({
|
||||
|
||||
console.log(`${new Date().toISOString()} - Enviando a generar plan IA`)
|
||||
|
||||
setIsSpinningIA(true)
|
||||
const plan = await generatePlanAI.mutateAsync(aiInput as any)
|
||||
setIsSpinningIA(false)
|
||||
console.log(`${new Date().toISOString()} - Plan IA generado`, plan)
|
||||
|
||||
navigate({
|
||||
@@ -108,12 +113,14 @@ export function WizardControls({
|
||||
return
|
||||
}
|
||||
} catch (err: any) {
|
||||
setIsSpinningIA(false)
|
||||
setWizard((w) => ({
|
||||
...w,
|
||||
isLoading: false,
|
||||
errorMessage: err?.message ?? 'Error generando el plan',
|
||||
}))
|
||||
} finally {
|
||||
setIsSpinningIA(false)
|
||||
setWizard((w) => ({ ...w, isLoading: false }))
|
||||
}
|
||||
}
|
||||
@@ -130,6 +137,17 @@ export function WizardControls({
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mx-2 flex w-5 items-center justify-center">
|
||||
<Loader2
|
||||
className={
|
||||
wizard.tipoOrigen === 'IA' && isSpinningIA
|
||||
? 'text-muted-foreground h-6 w-6 animate-spin'
|
||||
: 'h-6 w-6 opacity-0'
|
||||
}
|
||||
aria-hidden={!(wizard.tipoOrigen === 'IA' && isSpinningIA)}
|
||||
/>
|
||||
</div>
|
||||
{isLastStep ? (
|
||||
<Button onClick={handleCreate} disabled={disableCreate}>
|
||||
Crear plan
|
||||
|
||||
@@ -16,7 +16,7 @@ import type {
|
||||
AsignaturaSugerida,
|
||||
DataAsignaturaSugerida,
|
||||
} from '@/features/asignaturas/nueva/types'
|
||||
import type { Database } from '@/types/supabase'
|
||||
import type { Database, TablesInsert } from '@/types/supabase'
|
||||
|
||||
const EDGE = {
|
||||
generate_subject_suggestions: 'generate-subject-suggestions',
|
||||
@@ -89,23 +89,18 @@ export async function subjects_bibliografia_list(
|
||||
return data ?? []
|
||||
}
|
||||
|
||||
/** Wizard: crear asignatura manual (Edge Function) */
|
||||
export type SubjectsCreateManualInput = {
|
||||
planId: UUID
|
||||
datosBasicos: {
|
||||
nombre: string
|
||||
clave?: string
|
||||
tipo: TipoAsignatura
|
||||
creditos: number
|
||||
horasSemana?: number
|
||||
estructuraId: UUID
|
||||
}
|
||||
}
|
||||
|
||||
export async function subjects_create_manual(
|
||||
payload: SubjectsCreateManualInput,
|
||||
payload: TablesInsert<'asignaturas'>,
|
||||
): Promise<Asignatura> {
|
||||
return invokeEdge<Asignatura>(EDGE.subjects_create_manual, payload)
|
||||
const supabase = supabaseBrowser()
|
||||
const { data, error } = await supabase
|
||||
.from('asignaturas')
|
||||
.insert(payload)
|
||||
.select()
|
||||
.single()
|
||||
|
||||
throwIfError(error)
|
||||
return requireData(data, 'No se pudo crear la asignatura.')
|
||||
}
|
||||
|
||||
export type AIGenerateSubjectInput = {
|
||||
|
||||
@@ -77,6 +77,16 @@ export function usePlanAsignaturas(planId: UUID | null | undefined) {
|
||||
: ['planes', 'asignaturas', null],
|
||||
queryFn: () => plan_asignaturas_list(planId as UUID),
|
||||
enabled: Boolean(planId),
|
||||
|
||||
refetchInterval: (query) => {
|
||||
const data = query.state.data
|
||||
if (!Array.isArray(data)) return false
|
||||
const hayGenerando = data.some(
|
||||
(a: any) => (a as { estado?: unknown }).estado === 'generando',
|
||||
)
|
||||
return hayGenerando ? 500 : false
|
||||
},
|
||||
refetchIntervalInBackground: true,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -269,7 +279,7 @@ export function useDeleteLinea() {
|
||||
const qc = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: lineas_delete,
|
||||
onSuccess: (idEliminado) => {
|
||||
onSuccess: (_idEliminado) => {
|
||||
// Invalidamos para que las materias y líneas se refresquen
|
||||
qc.invalidateQueries({ queryKey: ['plan_lineas'] })
|
||||
qc.invalidateQueries({ queryKey: ['plan_asignaturas'] })
|
||||
|
||||
@@ -23,10 +23,10 @@ import { qk } from '../query/keys'
|
||||
|
||||
import type {
|
||||
BibliografiaUpsertInput,
|
||||
SubjectsCreateManualInput,
|
||||
SubjectsUpdateFieldsPatch,
|
||||
} from '../api/subjects.api'
|
||||
import type { UUID } from '../types/domain'
|
||||
import type { TablesInsert } from '@/types/supabase'
|
||||
|
||||
export function useSubject(subjectId: UUID | null | undefined) {
|
||||
return useQuery({
|
||||
@@ -82,7 +82,7 @@ export function useCreateSubjectManual() {
|
||||
const qc = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (payload: SubjectsCreateManualInput) =>
|
||||
mutationFn: (payload: TablesInsert<'asignaturas'>) =>
|
||||
subjects_create_manual(payload),
|
||||
onSuccess: (subject) => {
|
||||
qc.setQueryData(qk.asignatura(subject.id), subject)
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import { createFileRoute, Outlet, useNavigate } from '@tanstack/react-router'
|
||||
import {
|
||||
Plus,
|
||||
@@ -9,7 +8,7 @@ import {
|
||||
BookOpen,
|
||||
Loader2,
|
||||
} from 'lucide-react'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useMemo, useState } from 'react'
|
||||
|
||||
import type { Asignatura, AsignaturaStatus, TipoAsignatura } from '@/types/plan'
|
||||
import type { Tables } from '@/types/supabase'
|
||||
@@ -32,14 +31,18 @@ import {
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table'
|
||||
import { qk, supabaseBrowser, usePlanAsignaturas, usePlanLineas } from '@/data'
|
||||
import { usePlanAsignaturas, usePlanLineas } from '@/data'
|
||||
|
||||
// --- Configuración de Estilos ---
|
||||
const statusConfig: Record<
|
||||
AsignaturaStatus,
|
||||
{ label: string; className: string }
|
||||
> = {
|
||||
generando: { label: 'Generando', className: 'bg-slate-100 text-slate-600' },
|
||||
generando: {
|
||||
label: 'Generando',
|
||||
className:
|
||||
'bg-slate-100 text-slate-600 animate-pulse [animation-duration:2s]',
|
||||
},
|
||||
borrador: { label: 'Borrador', className: 'bg-slate-100 text-slate-600' },
|
||||
revisada: { label: 'Revisada', className: 'bg-amber-100 text-amber-700' },
|
||||
aprobada: { label: 'Aprobada', className: 'bg-emerald-100 text-emerald-700' },
|
||||
@@ -82,38 +85,12 @@ export const Route = createFileRoute('/planes/$planId/_detalle/asignaturas')({
|
||||
function AsignaturasPage() {
|
||||
const { planId } = Route.useParams()
|
||||
const navigate = useNavigate()
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
// 1. Fetch de datos reales
|
||||
const { data: asignaturasApi, isLoading: loadingAsig } =
|
||||
usePlanAsignaturas(planId)
|
||||
const { data: lineasApi, isLoading: loadingLineas } = usePlanLineas(planId)
|
||||
|
||||
useEffect(() => {
|
||||
const supabase = supabaseBrowser()
|
||||
const channel = supabase
|
||||
.channel(`plan:${planId}:asignaturas:updates`)
|
||||
.on(
|
||||
'postgres_changes',
|
||||
{
|
||||
event: 'UPDATE',
|
||||
schema: 'public',
|
||||
table: 'asignaturas',
|
||||
filter: `plan_estudio_id=eq.${planId}`,
|
||||
},
|
||||
() => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: qk.planAsignaturas(planId),
|
||||
})
|
||||
},
|
||||
)
|
||||
.subscribe()
|
||||
|
||||
return () => {
|
||||
supabase.removeChannel(channel)
|
||||
}
|
||||
}, [planId, queryClient])
|
||||
|
||||
// 2. Estados de filtrado
|
||||
const [searchTerm, setSearchTerm] = useState('')
|
||||
const [filterTipo, setFilterTipo] = useState<string>('all')
|
||||
|
||||
Reference in New Issue
Block a user