generación de plan con invalidación de queries

This commit is contained in:
2026-01-21 12:11:12 -06:00
parent 18f2bed3ea
commit 254f6383d7
3 changed files with 101 additions and 76 deletions

View File

@@ -1,24 +1,102 @@
import { useNavigate } from '@tanstack/react-router'
import type { NewPlanWizardState } from '@/features/planes/nuevo/types'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { useGeneratePlanAI } from '@/data/hooks/usePlans'
export function WizardControls({ export function WizardControls({
errorMessage, errorMessage,
onPrev, onPrev,
onNext, onNext,
onCreate,
disablePrev, disablePrev,
disableNext, disableNext,
disableCreate, disableCreate,
isLastStep, isLastStep,
wizard,
setWizard,
}: { }: {
errorMessage?: string | null errorMessage?: string | null
onPrev: () => void onPrev: () => void
onNext: () => void onNext: () => void
onCreate: () => void
disablePrev: boolean disablePrev: boolean
disableNext: boolean disableNext: boolean
disableCreate: boolean disableCreate: boolean
isLastStep: boolean isLastStep: boolean
wizard: NewPlanWizardState
setWizard: React.Dispatch<React.SetStateAction<NewPlanWizardState>>
}) { }) {
const navigate = useNavigate()
const generatePlanAI = useGeneratePlanAI()
// const persistPlanFromAI = usePersistPlanFromAI()
const handleCreate = async () => {
// Start loading
setWizard(
(w: NewPlanWizardState): NewPlanWizardState => ({
...w,
isLoading: true,
errorMessage: null,
}),
)
try {
if (wizard.tipoOrigen === 'IA') {
const tipoCicloSafe = (wizard.datosBasicos.tipoCiclo ||
'Semestre') as any
const numCiclosSafe =
typeof wizard.datosBasicos.numCiclos === 'number'
? wizard.datosBasicos.numCiclos
: 1
const aiInput = {
datosBasicos: {
nombrePlan: wizard.datosBasicos.nombrePlan,
carreraId: wizard.datosBasicos.carreraId,
facultadId: wizard.datosBasicos.facultadId || undefined,
nivel: wizard.datosBasicos.nivel as string,
tipoCiclo: tipoCicloSafe,
numCiclos: numCiclosSafe,
estructuraPlanId: wizard.datosBasicos.estructuraPlanId as string,
},
iaConfig: {
descripcionEnfoque: wizard.iaConfig?.descripcionEnfoque || '',
notasAdicionales: wizard.iaConfig?.notasAdicionales || '',
archivosReferencia: wizard.iaConfig?.archivosReferencia || [],
repositoriosIds: wizard.iaConfig?.repositoriosReferencia || [],
archivosAdjuntos: wizard.iaConfig?.archivosAdjuntos || [],
},
}
const data = await generatePlanAI.mutateAsync(aiInput as any)
// navigate({ to: `/planes/${data.plan.id}` })
return
}
// Fallback mocks for non-IA origins
await new Promise((r) => setTimeout(r, 900))
const nuevoId = (() => {
if (wizard.tipoOrigen === 'MANUAL') return 'plan_new_manual_001'
if (
wizard.tipoOrigen === 'CLONADO_INTERNO' ||
wizard.tipoOrigen === 'CLONADO_TRADICIONAL'
)
return 'plan_new_clone_001'
return 'plan_new_import_001'
})()
navigate({ to: `/planes/${nuevoId}` })
} catch (err: any) {
setWizard((w) => ({
...w,
isLoading: false,
errorMessage: err?.message ?? 'Error generando el plan',
}))
} finally {
setWizard((w) => ({ ...w, isLoading: false }))
}
}
return ( return (
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex-1"> <div className="flex-1">
@@ -33,7 +111,7 @@ export function WizardControls({
Anterior Anterior
</Button> </Button>
{isLastStep ? ( {isLastStep ? (
<Button onClick={onCreate} disabled={disableCreate}> <Button onClick={handleCreate} disabled={disableCreate}>
Crear plan Crear plan
</Button> </Button>
) : ( ) : (

View File

@@ -116,11 +116,25 @@ export function useCreatePlanManual() {
} }
export function useGeneratePlanAI() { export function useGeneratePlanAI() {
const qc = useQueryClient();
return useMutation({ return useMutation({
mutationFn: ai_generate_plan, mutationFn: ai_generate_plan,
onSuccess: (data) => {
// Asumiendo que la Edge Function devuelve { ok: true, plan: { id: ... } }
const newPlan = data.plan;
if (newPlan) {
// 1. Invalidar la lista para que aparezca el nuevo plan
qc.invalidateQueries({ queryKey: ["planes", "list"] });
// 2. (Opcional) Pre-cargar el dato individual para que la navegación sea instantánea
// qc.setQueryData(["planes", "detail", newPlan.id], newPlan);
}
},
}); });
} }
// Funcion obsoleta porque ahora el plan se persiste directamente en useGeneratePlanAI
export function usePersistPlanFromAI() { export function usePersistPlanFromAI() {
const qc = useQueryClient(); const qc = useQueryClient();

View File

@@ -3,7 +3,7 @@ import * as Icons from 'lucide-react'
import { useNuevoPlanWizard } from './hooks/useNuevoPlanWizard' import { useNuevoPlanWizard } from './hooks/useNuevoPlanWizard'
import type { NewPlanWizardState } from './types' // import type { NewPlanWizardState } from './types'
import { PasoBasicosForm } from '@/components/planes/wizard/PasoBasicosForm/PasoBasicosForm' import { PasoBasicosForm } from '@/components/planes/wizard/PasoBasicosForm/PasoBasicosForm'
import { PasoDetallesPanel } from '@/components/planes/wizard/PasoDetallesPanel/PasoDetallesPanel' import { PasoDetallesPanel } from '@/components/planes/wizard/PasoDetallesPanel/PasoDetallesPanel'
@@ -25,7 +25,7 @@ import {
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
} from '@/components/ui/dialog' } from '@/components/ui/dialog'
import { useGeneratePlanAI } from '@/data/hooks/usePlans' // import { useGeneratePlanAI } from '@/data/hooks/usePlans'
// Mock de permisos/rol // Mock de permisos/rol
const auth_get_current_user_role = () => 'JEFE_CARRERA' as const const auth_get_current_user_role = () => 'JEFE_CARRERA' as const
@@ -48,8 +48,7 @@ const Wizard = defineStepper(
export default function NuevoPlanModalContainer() { export default function NuevoPlanModalContainer() {
const navigate = useNavigate() const navigate = useNavigate()
const role = auth_get_current_user_role() const role = auth_get_current_user_role()
const generatePlanAI = useGeneratePlanAI() // const generatePlanAI = useGeneratePlanAI()
// const persistPlanFromAI = usePersistPlanFromAI()
const { const {
wizard, wizard,
@@ -63,74 +62,7 @@ export default function NuevoPlanModalContainer() {
navigate({ to: '/planes', resetScroll: false }) navigate({ to: '/planes', resetScroll: false })
} }
const crearPlan = async () => { // Crear plan: ahora la lógica vive en WizardControls
setWizard(
(w: NewPlanWizardState): NewPlanWizardState => ({
...w,
isLoading: true,
errorMessage: null,
}),
)
try {
if (wizard.tipoOrigen === 'IA') {
const tipoCicloSafe = (wizard.datosBasicos.tipoCiclo ||
'Semestre') as any
const numCiclosSafe =
typeof wizard.datosBasicos.numCiclos === 'number'
? wizard.datosBasicos.numCiclos
: 1
const aiInput = {
datosBasicos: {
nombrePlan: wizard.datosBasicos.nombrePlan,
carreraId: wizard.datosBasicos.carreraId,
facultadId: wizard.datosBasicos.facultadId || undefined,
nivel: wizard.datosBasicos.nivel as string,
tipoCiclo: tipoCicloSafe,
numCiclos: numCiclosSafe,
estructuraPlanId: wizard.datosBasicos.estructuraPlanId as string,
},
iaConfig: {
descripcionEnfoque: wizard.iaConfig?.descripcionEnfoque || '',
notasAdicionales: wizard.iaConfig?.notasAdicionales || '',
archivosReferencia: wizard.iaConfig?.archivosReferencia || [],
repositoriosIds: wizard.iaConfig?.repositoriosReferencia || [],
archivosAdjuntos: wizard.iaConfig?.archivosAdjuntos || [],
},
}
const response = await generatePlanAI.mutateAsync(aiInput as any)
// const createdPlan = await persistPlanFromAI.mutateAsync({
// jsonPlan: generatedJson,
// })
// navigate({ to: `/planes/${createdPlan.id}` })
console.log('Plan generado por IA:', response)
return
}
// Fallback: comportamiento previo para otros modos (mock IDs)
await new Promise((r) => setTimeout(r, 900))
const nuevoId = (() => {
if (wizard.tipoOrigen === 'MANUAL') return 'plan_new_manual_001'
if (
wizard.tipoOrigen === 'CLONADO_INTERNO' ||
wizard.tipoOrigen === 'CLONADO_TRADICIONAL'
)
return 'plan_new_clone_001'
return 'plan_new_import_001'
})()
navigate({ to: `/planes/${nuevoId}` })
} catch (err: any) {
setWizard((w) => ({
...w,
isLoading: false,
errorMessage: err?.message ?? 'Error generando el plan con IA',
}))
} finally {
setWizard((w) => ({ ...w, isLoading: false }))
}
}
return ( return (
<Dialog open={true} onOpenChange={(open) => !open && handleClose()}> <Dialog open={true} onOpenChange={(open) => !open && handleClose()}>
@@ -233,7 +165,6 @@ export default function NuevoPlanModalContainer() {
errorMessage={wizard.errorMessage} errorMessage={wizard.errorMessage}
onPrev={() => methods.prev()} onPrev={() => methods.prev()}
onNext={() => methods.next()} onNext={() => methods.next()}
onCreate={crearPlan}
disablePrev={ disablePrev={
Wizard.utils.getIndex(methods.current.id) === 0 || Wizard.utils.getIndex(methods.current.id) === 0 ||
wizard.isLoading wizard.isLoading
@@ -252,6 +183,8 @@ export default function NuevoPlanModalContainer() {
Wizard.utils.getIndex(methods.current.id) >= Wizard.utils.getIndex(methods.current.id) >=
Wizard.steps.length - 1 Wizard.steps.length - 1
} }
wizard={wizard}
setWizard={setWizard}
/> />
</Wizard.Stepper.Controls> </Wizard.Stepper.Controls>
</div> </div>