Merge branch 'main' of https://github.lci.ulsa.mx/Guillermo.Arrieta/acad-ia-2
This commit is contained in:
@@ -1,12 +1,19 @@
|
|||||||
import { useQueryClient } from '@tanstack/react-query'
|
import { useQueryClient } from '@tanstack/react-query'
|
||||||
import { useNavigate } from '@tanstack/react-router'
|
import { useNavigate } from '@tanstack/react-router'
|
||||||
|
import { Loader2 } from 'lucide-react'
|
||||||
|
import { useState } from 'react'
|
||||||
|
|
||||||
import type { AIGenerateSubjectInput, AIGenerateSubjectJsonInput } from '@/data'
|
import type { AIGenerateSubjectInput, AIGenerateSubjectJsonInput } from '@/data'
|
||||||
import type { NewSubjectWizardState } from '@/features/asignaturas/nueva/types'
|
import type { NewSubjectWizardState } from '@/features/asignaturas/nueva/types'
|
||||||
import type { TablesInsert } from '@/types/supabase'
|
import type { TablesInsert } from '@/types/supabase'
|
||||||
|
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { supabaseBrowser, useGenerateSubjectAI, qk } from '@/data'
|
import {
|
||||||
|
supabaseBrowser,
|
||||||
|
useGenerateSubjectAI,
|
||||||
|
qk,
|
||||||
|
useCreateSubjectManual,
|
||||||
|
} from '@/data'
|
||||||
|
|
||||||
export function WizardControls({
|
export function WizardControls({
|
||||||
wizard,
|
wizard,
|
||||||
@@ -32,6 +39,8 @@ export function WizardControls({
|
|||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const qc = useQueryClient()
|
const qc = useQueryClient()
|
||||||
const generateSubjectAI = useGenerateSubjectAI()
|
const generateSubjectAI = useGenerateSubjectAI()
|
||||||
|
const createSubjectManual = useCreateSubjectManual()
|
||||||
|
const [isSpinningIA, setIsSpinningIA] = useState(false)
|
||||||
const handleCreate = async () => {
|
const handleCreate = async () => {
|
||||||
setWizard((w) => ({
|
setWizard((w) => ({
|
||||||
...w,
|
...w,
|
||||||
@@ -40,7 +49,7 @@ export function WizardControls({
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (wizard.tipoOrigen === 'IA' || wizard.tipoOrigen === 'IA_SIMPLE') {
|
if (wizard.tipoOrigen === 'IA_SIMPLE') {
|
||||||
const aiInput: AIGenerateSubjectInput = {
|
const aiInput: AIGenerateSubjectInput = {
|
||||||
plan_estudio_id: wizard.plan_estudio_id,
|
plan_estudio_id: wizard.plan_estudio_id,
|
||||||
datosBasicos: {
|
datosBasicos: {
|
||||||
@@ -68,11 +77,14 @@ export function WizardControls({
|
|||||||
`${new Date().toISOString()} - Enviando a generar asignatura con IA`,
|
`${new Date().toISOString()} - Enviando a generar asignatura con IA`,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
setIsSpinningIA(true)
|
||||||
const asignatura = await generateSubjectAI.mutateAsync(aiInput)
|
const asignatura = await generateSubjectAI.mutateAsync(aiInput)
|
||||||
console.log(
|
// await new Promise((resolve) => setTimeout(resolve, 20000)) // debug
|
||||||
`${new Date().toISOString()} - Asignatura IA generada`,
|
setIsSpinningIA(false)
|
||||||
asignatura,
|
// console.log(
|
||||||
)
|
// `${new Date().toISOString()} - Asignatura IA generada`,
|
||||||
|
// asignatura,
|
||||||
|
// )
|
||||||
|
|
||||||
navigate({
|
navigate({
|
||||||
to: `/planes/${wizard.plan_estudio_id}/asignaturas/${asignatura.id}`,
|
to: `/planes/${wizard.plan_estudio_id}/asignaturas/${asignatura.id}`,
|
||||||
@@ -156,13 +168,40 @@ export function WizardControls({
|
|||||||
|
|
||||||
return
|
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) {
|
} catch (err: any) {
|
||||||
|
setIsSpinningIA(false)
|
||||||
setWizard((w) => ({
|
setWizard((w) => ({
|
||||||
...w,
|
...w,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
errorMessage: err?.message ?? 'Error creando la asignatura',
|
errorMessage: err?.message ?? 'Error creando la asignatura',
|
||||||
}))
|
}))
|
||||||
} finally {
|
} finally {
|
||||||
|
setIsSpinningIA(false)
|
||||||
setWizard((w) => ({ ...w, isLoading: false }))
|
setWizard((w) => ({ ...w, isLoading: false }))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -181,6 +220,17 @@ export function WizardControls({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</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 ? (
|
{isLastStep ? (
|
||||||
<Button onClick={handleCreate} disabled={disableCreate}>
|
<Button onClick={handleCreate} disabled={disableCreate}>
|
||||||
{wizard.isLoading ? 'Creando...' : 'Crear Asignatura'}
|
{wizard.isLoading ? 'Creando...' : 'Crear Asignatura'}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import { useNavigate } from '@tanstack/react-router'
|
import { useNavigate } from '@tanstack/react-router'
|
||||||
|
import { Loader2 } from 'lucide-react'
|
||||||
|
import { useState } from 'react'
|
||||||
|
|
||||||
import type { AIGeneratePlanInput } from '@/data'
|
import type { AIGeneratePlanInput } from '@/data'
|
||||||
import type { NivelPlanEstudio, TipoCiclo } from '@/data/types/domain'
|
import type { NivelPlanEstudio, TipoCiclo } from '@/data/types/domain'
|
||||||
@@ -33,6 +35,7 @@ export function WizardControls({
|
|||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const generatePlanAI = useGeneratePlanAI()
|
const generatePlanAI = useGeneratePlanAI()
|
||||||
const createPlanManual = useCreatePlanManual()
|
const createPlanManual = useCreatePlanManual()
|
||||||
|
const [isSpinningIA, setIsSpinningIA] = useState(false)
|
||||||
// const supabaseClient = supabaseBrowser()
|
// const supabaseClient = supabaseBrowser()
|
||||||
// const persistPlanFromAI = usePersistPlanFromAI()
|
// const persistPlanFromAI = usePersistPlanFromAI()
|
||||||
|
|
||||||
@@ -78,7 +81,9 @@ export function WizardControls({
|
|||||||
|
|
||||||
console.log(`${new Date().toISOString()} - Enviando a generar plan IA`)
|
console.log(`${new Date().toISOString()} - Enviando a generar plan IA`)
|
||||||
|
|
||||||
|
setIsSpinningIA(true)
|
||||||
const plan = await generatePlanAI.mutateAsync(aiInput as any)
|
const plan = await generatePlanAI.mutateAsync(aiInput as any)
|
||||||
|
setIsSpinningIA(false)
|
||||||
console.log(`${new Date().toISOString()} - Plan IA generado`, plan)
|
console.log(`${new Date().toISOString()} - Plan IA generado`, plan)
|
||||||
|
|
||||||
navigate({
|
navigate({
|
||||||
@@ -108,12 +113,14 @@ export function WizardControls({
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
|
setIsSpinningIA(false)
|
||||||
setWizard((w) => ({
|
setWizard((w) => ({
|
||||||
...w,
|
...w,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
errorMessage: err?.message ?? 'Error generando el plan',
|
errorMessage: err?.message ?? 'Error generando el plan',
|
||||||
}))
|
}))
|
||||||
} finally {
|
} finally {
|
||||||
|
setIsSpinningIA(false)
|
||||||
setWizard((w) => ({ ...w, isLoading: false }))
|
setWizard((w) => ({ ...w, isLoading: false }))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -130,6 +137,17 @@ export function WizardControls({
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</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 ? (
|
{isLastStep ? (
|
||||||
<Button onClick={handleCreate} disabled={disableCreate}>
|
<Button onClick={handleCreate} disabled={disableCreate}>
|
||||||
Crear plan
|
Crear plan
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import type {
|
|||||||
AsignaturaSugerida,
|
AsignaturaSugerida,
|
||||||
DataAsignaturaSugerida,
|
DataAsignaturaSugerida,
|
||||||
} from '@/features/asignaturas/nueva/types'
|
} from '@/features/asignaturas/nueva/types'
|
||||||
import type { Database } from '@/types/supabase'
|
import type { Database, TablesInsert } from '@/types/supabase'
|
||||||
|
|
||||||
const EDGE = {
|
const EDGE = {
|
||||||
generate_subject_suggestions: 'generate-subject-suggestions',
|
generate_subject_suggestions: 'generate-subject-suggestions',
|
||||||
@@ -89,23 +89,18 @@ export async function subjects_bibliografia_list(
|
|||||||
return data ?? []
|
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(
|
export async function subjects_create_manual(
|
||||||
payload: SubjectsCreateManualInput,
|
payload: TablesInsert<'asignaturas'>,
|
||||||
): Promise<Asignatura> {
|
): 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 = {
|
export type AIGenerateSubjectInput = {
|
||||||
|
|||||||
@@ -77,6 +77,16 @@ export function usePlanAsignaturas(planId: UUID | null | undefined) {
|
|||||||
: ['planes', 'asignaturas', null],
|
: ['planes', 'asignaturas', null],
|
||||||
queryFn: () => plan_asignaturas_list(planId as UUID),
|
queryFn: () => plan_asignaturas_list(planId as UUID),
|
||||||
enabled: Boolean(planId),
|
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()
|
const qc = useQueryClient()
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: lineas_delete,
|
mutationFn: lineas_delete,
|
||||||
onSuccess: (idEliminado) => {
|
onSuccess: (_idEliminado) => {
|
||||||
// Invalidamos para que las materias y líneas se refresquen
|
// Invalidamos para que las materias y líneas se refresquen
|
||||||
qc.invalidateQueries({ queryKey: ['plan_lineas'] })
|
qc.invalidateQueries({ queryKey: ['plan_lineas'] })
|
||||||
qc.invalidateQueries({ queryKey: ['plan_asignaturas'] })
|
qc.invalidateQueries({ queryKey: ['plan_asignaturas'] })
|
||||||
|
|||||||
@@ -23,10 +23,10 @@ import { qk } from '../query/keys'
|
|||||||
|
|
||||||
import type {
|
import type {
|
||||||
BibliografiaUpsertInput,
|
BibliografiaUpsertInput,
|
||||||
SubjectsCreateManualInput,
|
|
||||||
SubjectsUpdateFieldsPatch,
|
SubjectsUpdateFieldsPatch,
|
||||||
} from '../api/subjects.api'
|
} from '../api/subjects.api'
|
||||||
import type { UUID } from '../types/domain'
|
import type { UUID } from '../types/domain'
|
||||||
|
import type { TablesInsert } from '@/types/supabase'
|
||||||
|
|
||||||
export function useSubject(subjectId: UUID | null | undefined) {
|
export function useSubject(subjectId: UUID | null | undefined) {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
@@ -82,7 +82,7 @@ export function useCreateSubjectManual() {
|
|||||||
const qc = useQueryClient()
|
const qc = useQueryClient()
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: (payload: SubjectsCreateManualInput) =>
|
mutationFn: (payload: TablesInsert<'asignaturas'>) =>
|
||||||
subjects_create_manual(payload),
|
subjects_create_manual(payload),
|
||||||
onSuccess: (subject) => {
|
onSuccess: (subject) => {
|
||||||
qc.setQueryData(qk.asignatura(subject.id), 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 { createFileRoute, Outlet, useNavigate } from '@tanstack/react-router'
|
||||||
import {
|
import {
|
||||||
Plus,
|
Plus,
|
||||||
@@ -9,7 +8,7 @@ import {
|
|||||||
BookOpen,
|
BookOpen,
|
||||||
Loader2,
|
Loader2,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { useEffect, useMemo, useState } from 'react'
|
import { useMemo, useState } from 'react'
|
||||||
|
|
||||||
import type { Asignatura, AsignaturaStatus, TipoAsignatura } from '@/types/plan'
|
import type { Asignatura, AsignaturaStatus, TipoAsignatura } from '@/types/plan'
|
||||||
import type { Tables } from '@/types/supabase'
|
import type { Tables } from '@/types/supabase'
|
||||||
@@ -32,14 +31,18 @@ import {
|
|||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow,
|
||||||
} from '@/components/ui/table'
|
} from '@/components/ui/table'
|
||||||
import { qk, supabaseBrowser, usePlanAsignaturas, usePlanLineas } from '@/data'
|
import { usePlanAsignaturas, usePlanLineas } from '@/data'
|
||||||
|
|
||||||
// --- Configuración de Estilos ---
|
// --- Configuración de Estilos ---
|
||||||
const statusConfig: Record<
|
const statusConfig: Record<
|
||||||
AsignaturaStatus,
|
AsignaturaStatus,
|
||||||
{ label: string; className: string }
|
{ 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' },
|
borrador: { label: 'Borrador', className: 'bg-slate-100 text-slate-600' },
|
||||||
revisada: { label: 'Revisada', className: 'bg-amber-100 text-amber-700' },
|
revisada: { label: 'Revisada', className: 'bg-amber-100 text-amber-700' },
|
||||||
aprobada: { label: 'Aprobada', className: 'bg-emerald-100 text-emerald-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() {
|
function AsignaturasPage() {
|
||||||
const { planId } = Route.useParams()
|
const { planId } = Route.useParams()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const queryClient = useQueryClient()
|
|
||||||
|
|
||||||
// 1. Fetch de datos reales
|
// 1. Fetch de datos reales
|
||||||
const { data: asignaturasApi, isLoading: loadingAsig } =
|
const { data: asignaturasApi, isLoading: loadingAsig } =
|
||||||
usePlanAsignaturas(planId)
|
usePlanAsignaturas(planId)
|
||||||
const { data: lineasApi, isLoading: loadingLineas } = usePlanLineas(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
|
// 2. Estados de filtrado
|
||||||
const [searchTerm, setSearchTerm] = useState('')
|
const [searchTerm, setSearchTerm] = useState('')
|
||||||
const [filterTipo, setFilterTipo] = useState<string>('all')
|
const [filterTipo, setFilterTipo] = useState<string>('all')
|
||||||
|
|||||||
Reference in New Issue
Block a user