Creación de planes de estudio con polling debido a mandar crear los datos en segundo plano
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useNavigate } from '@tanstack/react-router'
|
||||
import { Loader2 } from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
|
||||
import type { AIGeneratePlanInput } from '@/data'
|
||||
import type { NivelPlanEstudio, TipoCiclo } from '@/data/types/domain'
|
||||
@@ -8,8 +9,13 @@ import type { NewPlanWizardState } from '@/features/planes/nuevo/types'
|
||||
// import type { Database } from '@/types/supabase'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
// import { supabaseBrowser } from '@/data'
|
||||
import { useCreatePlanManual, useGeneratePlanAI } from '@/data/hooks/usePlans'
|
||||
import { plans_get_maybe } from '@/data/api/plans.api'
|
||||
import {
|
||||
useCreatePlanManual,
|
||||
useDeletePlanEstudio,
|
||||
useGeneratePlanAI,
|
||||
} from '@/data/hooks/usePlans'
|
||||
import { qk } from '@/data/query/keys'
|
||||
|
||||
export function WizardControls({
|
||||
errorMessage,
|
||||
@@ -35,10 +41,88 @@ export function WizardControls({
|
||||
const navigate = useNavigate()
|
||||
const generatePlanAI = useGeneratePlanAI()
|
||||
const createPlanManual = useCreatePlanManual()
|
||||
const deletePlan = useDeletePlanEstudio()
|
||||
const [isSpinningIA, setIsSpinningIA] = useState(false)
|
||||
const [pollPlanId, setPollPlanId] = useState<string | null>(null)
|
||||
const cancelledRef = useRef(false)
|
||||
// const supabaseClient = supabaseBrowser()
|
||||
// const persistPlanFromAI = usePersistPlanFromAI()
|
||||
|
||||
useEffect(() => {
|
||||
cancelledRef.current = false
|
||||
return () => {
|
||||
cancelledRef.current = true
|
||||
}
|
||||
}, [])
|
||||
|
||||
const planQuery = useQuery({
|
||||
queryKey: pollPlanId
|
||||
? qk.planMaybe(pollPlanId)
|
||||
: ['planes', 'detail-maybe', null],
|
||||
queryFn: () => plans_get_maybe(pollPlanId as string),
|
||||
enabled: Boolean(pollPlanId),
|
||||
refetchInterval: pollPlanId ? 3000 : false,
|
||||
refetchIntervalInBackground: true,
|
||||
staleTime: 0,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (!pollPlanId) return
|
||||
if (cancelledRef.current) return
|
||||
|
||||
// Si aún no existe en BDD, seguimos esperando.
|
||||
const plan = planQuery.data
|
||||
if (!plan) return
|
||||
|
||||
const clave = String(plan.estados_plan?.clave ?? '').toUpperCase()
|
||||
|
||||
if (clave.startsWith('GENERANDO')) return
|
||||
|
||||
if (clave.startsWith('BORRADOR')) {
|
||||
setPollPlanId(null)
|
||||
setIsSpinningIA(false)
|
||||
setWizard((w) => ({ ...w, isLoading: false }))
|
||||
navigate({
|
||||
to: `/planes/${plan.id}`,
|
||||
state: { showConfetti: true },
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (clave.startsWith('FALLID')) {
|
||||
// Detenemos el polling primero para evitar loops.
|
||||
setPollPlanId(null)
|
||||
setIsSpinningIA(false)
|
||||
|
||||
deletePlan
|
||||
.mutateAsync(plan.id)
|
||||
.catch(() => {
|
||||
// Si falla el borrado, igual mostramos el error.
|
||||
})
|
||||
.finally(() => {
|
||||
setWizard((w) => ({
|
||||
...w,
|
||||
isLoading: false,
|
||||
errorMessage: 'La generación del plan falló',
|
||||
}))
|
||||
})
|
||||
}
|
||||
}, [pollPlanId, planQuery.data, navigate, setWizard, deletePlan])
|
||||
|
||||
useEffect(() => {
|
||||
if (!pollPlanId) return
|
||||
if (!planQuery.isError) return
|
||||
setPollPlanId(null)
|
||||
setIsSpinningIA(false)
|
||||
setWizard((w) => ({
|
||||
...w,
|
||||
isLoading: false,
|
||||
errorMessage:
|
||||
(planQuery.error as any)?.message ??
|
||||
'Error consultando el estado del plan',
|
||||
}))
|
||||
}, [pollPlanId, planQuery.isError, planQuery.error, setWizard])
|
||||
|
||||
const handleCreate = async () => {
|
||||
// Start loading
|
||||
setWizard(
|
||||
@@ -82,14 +166,16 @@ 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)
|
||||
const resp: any = await generatePlanAI.mutateAsync(aiInput as any)
|
||||
const planId = resp?.plan?.id ?? resp?.id
|
||||
console.log(`${new Date().toISOString()} - Plan IA generado`, resp)
|
||||
|
||||
navigate({
|
||||
to: `/planes/${plan.id}`,
|
||||
state: { showConfetti: true },
|
||||
})
|
||||
if (!planId) {
|
||||
throw new Error('No se pudo obtener el id del plan generado por IA')
|
||||
}
|
||||
|
||||
// Inicia polling con React Query; el efecto navega o marca error.
|
||||
setPollPlanId(String(planId))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -114,16 +200,20 @@ export function WizardControls({
|
||||
}
|
||||
} catch (err: any) {
|
||||
setIsSpinningIA(false)
|
||||
setPollPlanId(null)
|
||||
setWizard((w) => ({
|
||||
...w,
|
||||
isLoading: false,
|
||||
errorMessage: err?.message ?? 'Error generando el plan',
|
||||
}))
|
||||
} finally {
|
||||
// Si entramos en polling, el loading se corta desde el efecto terminal.
|
||||
if (!pollPlanId) {
|
||||
setIsSpinningIA(false)
|
||||
setWizard((w) => ({ ...w, isLoading: false }))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex grow items-center justify-between">
|
||||
|
||||
@@ -144,6 +144,48 @@ export async function plans_get(planId: UUID): Promise<PlanEstudio> {
|
||||
return requireData(data, 'Plan no encontrado.')
|
||||
}
|
||||
|
||||
/**
|
||||
* Variante de `plans_get` que NO lanza si no existe (devuelve null).
|
||||
* Útil para flujos de polling donde el plan puede tardar en aparecer.
|
||||
*/
|
||||
export async function plans_get_maybe(
|
||||
planId: UUID,
|
||||
): Promise<PlanEstudio | null> {
|
||||
const supabase = supabaseBrowser()
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('planes_estudio')
|
||||
.select(
|
||||
`
|
||||
*,
|
||||
carreras (*, facultades(*)),
|
||||
estructuras_plan (*),
|
||||
estados_plan (*)
|
||||
`,
|
||||
)
|
||||
.eq('id', planId)
|
||||
.maybeSingle()
|
||||
|
||||
throwIfError(error)
|
||||
return (data ?? null) as unknown as PlanEstudio | null
|
||||
}
|
||||
|
||||
export async function plans_delete(planId: UUID): Promise<{ id: UUID }> {
|
||||
const supabase = supabaseBrowser()
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('planes_estudio')
|
||||
.delete()
|
||||
.eq('id', planId)
|
||||
.select('id')
|
||||
.maybeSingle()
|
||||
|
||||
throwIfError(error)
|
||||
|
||||
// Si por alguna razón no retorna fila (RLS / triggers), devolvemos el id solicitado.
|
||||
return { id: ((data as any)?.id ?? planId) as UUID }
|
||||
}
|
||||
|
||||
export async function plan_lineas_list(
|
||||
planId: UUID,
|
||||
): Promise<Array<LineaPlan>> {
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
plan_lineas_list,
|
||||
plans_clone_from_existing,
|
||||
plans_create_manual,
|
||||
plans_delete,
|
||||
plans_generate_document,
|
||||
plans_get,
|
||||
plans_get_document,
|
||||
@@ -263,6 +264,23 @@ export function useTransitionPlanEstado() {
|
||||
})
|
||||
}
|
||||
|
||||
export function useDeletePlanEstudio() {
|
||||
const qc = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (planId: UUID) => plans_delete(planId),
|
||||
onSuccess: (_ok, planId) => {
|
||||
qc.invalidateQueries({ queryKey: ['planes', 'list'] })
|
||||
qc.removeQueries({ queryKey: qk.plan(planId) })
|
||||
qc.removeQueries({ queryKey: qk.planMaybe(planId) })
|
||||
qc.removeQueries({ queryKey: qk.planAsignaturas(planId) })
|
||||
qc.removeQueries({ queryKey: qk.planLineas(planId) })
|
||||
qc.removeQueries({ queryKey: qk.planHistorial(planId) })
|
||||
qc.removeQueries({ queryKey: qk.planDocumento(planId) })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function useGeneratePlanDocumento() {
|
||||
const qc = useQueryClient()
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ export const qk = {
|
||||
|
||||
planesList: (filters: unknown) => ['planes', 'list', filters] as const,
|
||||
plan: (planId: string) => ['planes', 'detail', planId] as const,
|
||||
planMaybe: (planId: string) => ['planes', 'detail-maybe', planId] as const,
|
||||
planLineas: (planId: string) => ['planes', planId, 'lineas'] as const,
|
||||
planAsignaturas: (planId: string) =>
|
||||
['planes', planId, 'asignaturas'] as const,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user