fix #114: refactorización de AsignaturaDetailPage y hooks relacionados: persistencia, caché y tipado
- Persistencia de cambios de "Datos generales" usando updateAsignatura.mutate. - Corregido el manejo de caché: uso de qk centralizada y merge en setQueryData para no perder relaciones. - Corregidos los tipos devueltos por subjects_get. - Evitado estado inválido tras guardar (merge local + actualización de cache). Verificar: editar → guardar → volver al plan → reingresar muestra datos actualizados sin parpadeos.
This commit is contained in:
@@ -14,6 +14,7 @@ import { DocumentoSEPTab } from './DocumentoSEPTab'
|
|||||||
import { HistorialTab } from './HistorialTab'
|
import { HistorialTab } from './HistorialTab'
|
||||||
import { IAAsignaturaTab } from './IAAsignaturaTab'
|
import { IAAsignaturaTab } from './IAAsignaturaTab'
|
||||||
|
|
||||||
|
import type { AsignaturaDetail } from '@/data'
|
||||||
import type {
|
import type {
|
||||||
CampoEstructura,
|
CampoEstructura,
|
||||||
IAMessage,
|
IAMessage,
|
||||||
@@ -32,7 +33,7 @@ import {
|
|||||||
TooltipProvider,
|
TooltipProvider,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from '@/components/ui/tooltip'
|
} from '@/components/ui/tooltip'
|
||||||
import { useSubject } from '@/data/hooks/useSubjects'
|
import { useSubject, useUpdateAsignatura } from '@/data/hooks/useSubjects'
|
||||||
import {
|
import {
|
||||||
mockAsignatura,
|
mockAsignatura,
|
||||||
mockEstructura,
|
mockEstructura,
|
||||||
@@ -117,13 +118,14 @@ export default function AsignaturaDetailPage() {
|
|||||||
const { planId } = useParams({
|
const { planId } = useParams({
|
||||||
from: '/planes/$planId/asignaturas/$asignaturaId',
|
from: '/planes/$planId/asignaturas/$asignaturaId',
|
||||||
})
|
})
|
||||||
const { data: asignaturasApi, isLoading: loadingAsig } =
|
const { data: asignaturaApi, isLoading: loadingAsig } =
|
||||||
useSubject(asignaturaId)
|
useSubject(asignaturaId)
|
||||||
// 1. Asegúrate de tener estos estados en tu componente principal
|
// 1. Asegúrate de tener estos estados en tu componente principal
|
||||||
const [messages, setMessages] = useState<Array<IAMessage>>([])
|
const [messages, setMessages] = useState<Array<IAMessage>>([])
|
||||||
const [datosGenerales, setDatosGenerales] = useState({})
|
const [asignatura, setAsignatura] = useState({})
|
||||||
const [campos, setCampos] = useState<Array<CampoEstructura>>([])
|
const [campos, setCampos] = useState<Array<CampoEstructura>>([])
|
||||||
const [activeTab, setActiveTab] = useState('datos')
|
const [activeTab, setActiveTab] = useState('datos')
|
||||||
|
const updateAsignatura = useUpdateAsignatura()
|
||||||
|
|
||||||
// Dentro de AsignaturaDetailPage
|
// Dentro de AsignaturaDetailPage
|
||||||
const [headerData, setHeaderData] = useState({
|
const [headerData, setHeaderData] = useState({
|
||||||
@@ -142,27 +144,59 @@ export default function AsignaturaDetailPage() {
|
|||||||
|
|
||||||
// Sincronizar cuando llegue la API
|
// Sincronizar cuando llegue la API
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (asignaturasApi) {
|
if (asignaturaApi) {
|
||||||
setHeaderData({
|
setHeaderData({
|
||||||
codigo: asignaturasApi.codigo ?? '',
|
codigo: asignaturaApi.codigo ?? '',
|
||||||
nombre: asignaturasApi.nombre,
|
nombre: asignaturaApi.nombre,
|
||||||
creditos: asignaturasApi.creditos,
|
creditos: asignaturaApi.creditos,
|
||||||
ciclo: asignaturasApi.numero_ciclo ?? 0,
|
ciclo: asignaturaApi.numero_ciclo ?? 0,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}, [asignaturasApi])
|
}, [asignaturaApi])
|
||||||
|
|
||||||
const handleUpdateHeader = (key: string, value: string | number) => {
|
const handleUpdateHeader = (key: string, value: string | number) => {
|
||||||
const newData = { ...headerData, [key]: value }
|
const newData = { ...headerData, [key]: value }
|
||||||
setHeaderData(newData)
|
setHeaderData(newData)
|
||||||
console.log('💾 Guardando en estado y base de datos:', key, value)
|
|
||||||
|
const patch: Record<string, any> =
|
||||||
|
key === 'ciclo'
|
||||||
|
? { numero_ciclo: value }
|
||||||
|
: {
|
||||||
|
[key]: value,
|
||||||
|
}
|
||||||
|
|
||||||
|
updateAsignatura.mutate({
|
||||||
|
asignaturaId,
|
||||||
|
patch,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePersistDatoGeneral = (clave: string, value: string) => {
|
||||||
|
const baseDatos =
|
||||||
|
(asignatura as any)?.datos ?? (asignaturaApi as any)?.datos ?? {}
|
||||||
|
const mergedDatos = { ...baseDatos, [clave]: value }
|
||||||
|
|
||||||
|
// Mantener estado local coherente para merges posteriores.
|
||||||
|
setAsignatura((prev: any) => ({
|
||||||
|
...(prev && Object.keys(prev).length
|
||||||
|
? prev
|
||||||
|
: ((asignaturaApi as any) ?? {})),
|
||||||
|
datos: mergedDatos,
|
||||||
|
}))
|
||||||
|
|
||||||
|
updateAsignatura.mutate({
|
||||||
|
asignaturaId,
|
||||||
|
patch: {
|
||||||
|
datos: mergedDatos,
|
||||||
|
},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
/* ---------- sincronizar API ---------- */
|
/* ---------- sincronizar API ---------- */
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (asignaturasApi?.datos) {
|
if (asignaturaApi?.datos) {
|
||||||
setDatosGenerales(asignaturasApi)
|
setAsignatura(asignaturaApi)
|
||||||
}
|
}
|
||||||
}, [asignaturasApi])
|
}, [asignaturaApi])
|
||||||
|
|
||||||
// 2. Funciones de manejo para la IA
|
// 2. Funciones de manejo para la IA
|
||||||
const handleSendMessage = (text: string, campoId?: string) => {
|
const handleSendMessage = (text: string, campoId?: string) => {
|
||||||
@@ -180,7 +214,7 @@ export default function AsignaturaDetailPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleAcceptSuggestion = (sugerencia: IASugerencia) => {
|
const handleAcceptSuggestion = (sugerencia: IASugerencia) => {
|
||||||
// Lógica para actualizar el valor del campo en tu estado de datosGenerales
|
// Lógica para actualizar el valor del campo en tu estado de asignatura
|
||||||
// toast.success(`Sugerencia aplicada a ${sugerencia.campoNombre}`);
|
// toast.success(`Sugerencia aplicada a ${sugerencia.campoNombre}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -250,13 +284,13 @@ export default function AsignaturaDetailPage() {
|
|||||||
<span className="flex items-center gap-1">
|
<span className="flex items-center gap-1">
|
||||||
<GraduationCap className="h-4 w-4 shrink-0" />
|
<GraduationCap className="h-4 w-4 shrink-0" />
|
||||||
<span className="text-blue-100">
|
<span className="text-blue-100">
|
||||||
{asignaturasApi?.planes_estudio?.datos?.nombre || ''}
|
{asignaturaApi?.planes_estudio?.datos?.nombre || ''}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span className="flex items-center gap-1">
|
<span className="flex items-center gap-1">
|
||||||
<span className="text-blue-100">
|
<span className="text-blue-100">
|
||||||
{asignaturasApi?.planes_estudio?.carreras?.facultades
|
{asignaturaApi?.planes_estudio?.carreras?.facultades
|
||||||
?.nombre || ''}
|
?.nombre || ''}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
@@ -265,7 +299,7 @@ export default function AsignaturaDetailPage() {
|
|||||||
<p className="text-sm text-blue-300">
|
<p className="text-sm text-blue-300">
|
||||||
Pertenece al plan:{' '}
|
Pertenece al plan:{' '}
|
||||||
<span className="cursor-pointer underline">
|
<span className="cursor-pointer underline">
|
||||||
{asignaturasApi?.planes_estudio?.nombre}
|
{asignaturaApi?.planes_estudio?.nombre}
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -295,7 +329,7 @@ export default function AsignaturaDetailPage() {
|
|||||||
<span>° ciclo</span>
|
<span>° ciclo</span>
|
||||||
</Badge>
|
</Badge>
|
||||||
|
|
||||||
<Badge variant="secondary">{asignaturasApi?.tipo}</Badge>
|
<Badge variant="secondary">{asignaturaApi?.tipo}</Badge>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -323,15 +357,16 @@ export default function AsignaturaDetailPage() {
|
|||||||
{/* ================= TAB: DATOS GENERALES ================= */}
|
{/* ================= TAB: DATOS GENERALES ================= */}
|
||||||
<TabsContent value="datos">
|
<TabsContent value="datos">
|
||||||
<DatosGenerales
|
<DatosGenerales
|
||||||
data={datosGenerales}
|
data={asignatura}
|
||||||
isLoading={loadingAsig}
|
isLoading={loadingAsig}
|
||||||
asignaturaId={asignaturaId}
|
asignaturaId={asignaturaId}
|
||||||
|
onPersistDato={handlePersistDatoGeneral}
|
||||||
/>
|
/>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="contenido">
|
<TabsContent value="contenido">
|
||||||
<ContenidoTematico
|
<ContenidoTematico
|
||||||
data={asignaturasApi}
|
data={asignaturaApi}
|
||||||
isLoading={loadingAsig}
|
isLoading={loadingAsig}
|
||||||
></ContenidoTematico>
|
></ContenidoTematico>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
@@ -348,7 +383,7 @@ export default function AsignaturaDetailPage() {
|
|||||||
<TabsContent value="ia">
|
<TabsContent value="ia">
|
||||||
<IAAsignaturaTab
|
<IAAsignaturaTab
|
||||||
campos={campos}
|
campos={campos}
|
||||||
datosGenerales={datosGenerales}
|
asignatura={asignatura}
|
||||||
messages={messages}
|
messages={messages}
|
||||||
onSendMessage={handleSendMessage}
|
onSendMessage={handleSendMessage}
|
||||||
onAcceptSuggestion={handleAcceptSuggestion}
|
onAcceptSuggestion={handleAcceptSuggestion}
|
||||||
@@ -364,9 +399,9 @@ export default function AsignaturaDetailPage() {
|
|||||||
<TabsContent value="sep">
|
<TabsContent value="sep">
|
||||||
<DocumentoSEPTab
|
<DocumentoSEPTab
|
||||||
documento={mockDocumentoSep}
|
documento={mockDocumentoSep}
|
||||||
asignatura={mockAsignatura}
|
|
||||||
estructura={mockEstructura}
|
estructura={mockEstructura}
|
||||||
datosGenerales={datosGenerales}
|
asignatura={mockAsignatura}
|
||||||
|
datosGenerales={(asignatura as any)?.datos ?? {}}
|
||||||
onRegenerate={handleRegenerateDocument}
|
onRegenerate={handleRegenerateDocument}
|
||||||
isRegenerating={isRegenerating}
|
isRegenerating={isRegenerating}
|
||||||
/>
|
/>
|
||||||
@@ -385,23 +420,25 @@ export default function AsignaturaDetailPage() {
|
|||||||
/* ================= TAB CONTENT ================= */
|
/* ================= TAB CONTENT ================= */
|
||||||
interface DatosGeneralesProps {
|
interface DatosGeneralesProps {
|
||||||
asignaturaId: string
|
asignaturaId: string
|
||||||
data: AsignaturaDatos
|
data: AsignaturaDetail
|
||||||
isLoading: boolean
|
isLoading: boolean
|
||||||
|
onPersistDato: (clave: string, value: string) => void
|
||||||
}
|
}
|
||||||
function DatosGenerales({
|
function DatosGenerales({
|
||||||
data,
|
data,
|
||||||
isLoading,
|
isLoading,
|
||||||
asignaturaId,
|
asignaturaId,
|
||||||
|
onPersistDato,
|
||||||
}: DatosGeneralesProps) {
|
}: DatosGeneralesProps) {
|
||||||
const formatTitle = (key: string): string =>
|
const formatTitle = (key: string): string =>
|
||||||
key.replace(/_/g, ' ').replace(/\b\w/g, (l: string) => l.toUpperCase())
|
key.replace(/_/g, ' ').replace(/\b\w/g, (l: string) => l.toUpperCase())
|
||||||
|
|
||||||
// 1. Extraemos la definición de la estructura (los metadatos)
|
// 1. Extraemos la definición de la estructura (los metadatos)
|
||||||
const structureProps =
|
const structureProps =
|
||||||
data?.estructuras_asignatura?.definicion?.properties || {}
|
data.estructuras_asignatura?.definicion?.properties || {}
|
||||||
|
|
||||||
// 2. Extraemos los valores reales (el contenido redactado)
|
// 2. Extraemos los valores reales (el contenido redactado)
|
||||||
const valoresActuales = data?.datos || {}
|
const valoresActuales = data.datos || {}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="animate-in fade-in mx-auto max-w-7xl space-y-8 px-4 py-8 duration-500">
|
<div className="animate-in fade-in mx-auto max-w-7xl space-y-8 px-4 py-8 duration-500">
|
||||||
@@ -454,6 +491,7 @@ function DatosGenerales({
|
|||||||
placeholder={placeholder} // Aquí irá "Primer semestre", "MAT-101", etc.
|
placeholder={placeholder} // Aquí irá "Primer semestre", "MAT-101", etc.
|
||||||
description={description} // El texto largo de "Indicar el ciclo..."
|
description={description} // El texto largo de "Indicar el ciclo..."
|
||||||
onEnhanceAI={(contenido) => console.log(contenido)}
|
onEnhanceAI={(contenido) => console.log(contenido)}
|
||||||
|
onPersist={(clave, value) => onPersistDato(clave, value)}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -509,6 +547,7 @@ interface InfoCardProps {
|
|||||||
required?: boolean // Nueva prop para el asterisco
|
required?: boolean // Nueva prop para el asterisco
|
||||||
type?: 'text' | 'requirements' | 'evaluation'
|
type?: 'text' | 'requirements' | 'evaluation'
|
||||||
onEnhanceAI?: (content: any) => void
|
onEnhanceAI?: (content: any) => void
|
||||||
|
onPersist?: (clave: string, value: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
function InfoCard({
|
function InfoCard({
|
||||||
@@ -520,11 +559,15 @@ function InfoCard({
|
|||||||
description,
|
description,
|
||||||
required,
|
required,
|
||||||
type = 'text',
|
type = 'text',
|
||||||
|
onPersist,
|
||||||
}: InfoCardProps) {
|
}: InfoCardProps) {
|
||||||
const [isEditing, setIsEditing] = useState(false)
|
const [isEditing, setIsEditing] = useState(false)
|
||||||
const [data, setData] = useState(initialContent)
|
const [data, setData] = useState(initialContent)
|
||||||
const [tempText, setTempText] = useState(initialContent)
|
const [tempText, setTempText] = useState(initialContent)
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
const { planId } = useParams({
|
||||||
|
from: '/planes/$planId/asignaturas/$asignaturaId',
|
||||||
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setData(initialContent)
|
setData(initialContent)
|
||||||
@@ -532,9 +575,14 @@ function InfoCard({
|
|||||||
}, [initialContent])
|
}, [initialContent])
|
||||||
|
|
||||||
const handleSave = () => {
|
const handleSave = () => {
|
||||||
|
console.log('clave, valor:', clave, String(tempText ?? ''))
|
||||||
|
|
||||||
setData(tempText)
|
setData(tempText)
|
||||||
setIsEditing(false)
|
setIsEditing(false)
|
||||||
// Aquí iría tu lógica de guardado a la DB
|
|
||||||
|
if (type === 'text' && clave && onPersist) {
|
||||||
|
onPersist(clave, String(tempText ?? ''))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleIARequest = (campoClave: string) => {
|
const handleIARequest = (campoClave: string) => {
|
||||||
@@ -542,7 +590,7 @@ function InfoCard({
|
|||||||
|
|
||||||
navigate({
|
navigate({
|
||||||
to: '/planes/$planId/asignaturas/$asignaturaId',
|
to: '/planes/$planId/asignaturas/$asignaturaId',
|
||||||
params: { asignaturaId: asignaturaId! },
|
params: { planId, asignaturaId: asignaturaId! },
|
||||||
state: {
|
state: {
|
||||||
activeTab: 'ia',
|
activeTab: 'ia',
|
||||||
prefillCampo: campoClave,
|
prefillCampo: campoClave,
|
||||||
@@ -586,7 +634,7 @@ function InfoCard({
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="h-8 w-8 text-blue-500 hover:bg-blue-100"
|
className="h-8 w-8 text-blue-500 hover:bg-blue-100"
|
||||||
onClick={() => handleIARequest(clave)}
|
onClick={() => clave && handleIARequest(clave)}
|
||||||
>
|
>
|
||||||
<Sparkles className="h-4 w-4" />
|
<Sparkles className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,16 +1,19 @@
|
|||||||
import { useState } from 'react'
|
|
||||||
import {
|
import {
|
||||||
FileText,
|
FileText,
|
||||||
Download,
|
Download,
|
||||||
RefreshCw,
|
RefreshCw,
|
||||||
Calendar,
|
|
||||||
FileCheck,
|
FileCheck,
|
||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
Loader2,
|
Loader2,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
import { useState } from 'react'
|
||||||
import { Button } from '@/components/ui/button'
|
|
||||||
import { Badge } from '@/components/ui/badge'
|
import type {
|
||||||
|
DocumentoAsignatura,
|
||||||
|
Asignatura,
|
||||||
|
AsignaturaStructure,
|
||||||
|
} from '@/types/asignatura'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
AlertDialogAction,
|
AlertDialogAction,
|
||||||
@@ -22,16 +25,13 @@ import {
|
|||||||
AlertDialogTitle,
|
AlertDialogTitle,
|
||||||
AlertDialogTrigger,
|
AlertDialogTrigger,
|
||||||
} from '@/components/ui/alert-dialog'
|
} from '@/components/ui/alert-dialog'
|
||||||
import type {
|
import { Badge } from '@/components/ui/badge'
|
||||||
DocumentoAsignatura,
|
import { Button } from '@/components/ui/button'
|
||||||
Asignatura,
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
AsignaturaStructure,
|
|
||||||
} from '@/types/asignatura'
|
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import { useSubjectBibliografia } from '@/data/hooks/useSubjects'
|
// import { toast } from 'sonner';
|
||||||
//import { toast } from 'sonner';
|
// import { format } from 'date-fns';
|
||||||
//import { format } from 'date-fns';
|
// import { es } from 'date-fns/locale';
|
||||||
//import { es } from 'date-fns/locale';
|
|
||||||
|
|
||||||
interface DocumentoSEPTabProps {
|
interface DocumentoSEPTabProps {
|
||||||
documento: DocumentoAsignatura | null
|
documento: DocumentoAsignatura | null
|
||||||
@@ -45,8 +45,8 @@ interface DocumentoSEPTabProps {
|
|||||||
export function DocumentoSEPTab({
|
export function DocumentoSEPTab({
|
||||||
documento,
|
documento,
|
||||||
asignatura,
|
asignatura,
|
||||||
estructura,
|
|
||||||
datosGenerales,
|
datosGenerales,
|
||||||
|
estructura,
|
||||||
onRegenerate,
|
onRegenerate,
|
||||||
isRegenerating,
|
isRegenerating,
|
||||||
}: DocumentoSEPTabProps) {
|
}: DocumentoSEPTabProps) {
|
||||||
@@ -65,7 +65,7 @@ export function DocumentoSEPTab({
|
|||||||
const handleRegenerate = () => {
|
const handleRegenerate = () => {
|
||||||
setShowConfirmDialog(false)
|
setShowConfirmDialog(false)
|
||||||
onRegenerate()
|
onRegenerate()
|
||||||
//toast.success('Regenerando documento...');
|
// toast.success('Regenerando documento...');
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -86,7 +86,9 @@ export function DocumentoSEPTab({
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={
|
onClick={
|
||||||
() =>
|
() =>
|
||||||
console.log('descargando') /*toast.info('Descarga iniciada')*/
|
console.log(
|
||||||
|
'descargando',
|
||||||
|
) /* toast.info('Descarga iniciada')*/
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Download className="mr-2 h-4 w-4" />
|
<Download className="mr-2 h-4 w-4" />
|
||||||
@@ -202,7 +204,7 @@ export function DocumentoSEPTab({
|
|||||||
<div className="text-muted-foreground mt-8 border-t pt-6 text-center text-xs">
|
<div className="text-muted-foreground mt-8 border-t pt-6 text-center text-xs">
|
||||||
<p>
|
<p>
|
||||||
Documento generado el{' '}
|
Documento generado el{' '}
|
||||||
{/*format(documento.fechaGeneracion, "d 'de' MMMM 'de' yyyy", { locale: es })*/}
|
{/* format(documento.fechaGeneracion, "d 'de' MMMM 'de' yyyy", { locale: es })*/}
|
||||||
</p>
|
</p>
|
||||||
<p className="mt-1">Universidad La Salle</p>
|
<p className="mt-1">Universidad La Salle</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -261,7 +263,7 @@ export function DocumentoSEPTab({
|
|||||||
Generado
|
Generado
|
||||||
</span>
|
</span>
|
||||||
<span className="text-sm">
|
<span className="text-sm">
|
||||||
{/*format(documento.fechaGeneracion, "d MMM yyyy, HH:mm", { locale: es })*/}
|
{/* format(documento.fechaGeneracion, "d MMM yyyy, HH:mm", { locale: es })*/}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ interface SelectedField {
|
|||||||
|
|
||||||
interface IAAsignaturaTabProps {
|
interface IAAsignaturaTabProps {
|
||||||
campos: Array<CampoEstructura>
|
campos: Array<CampoEstructura>
|
||||||
datosGenerales: Record<string, any>
|
asignatura: Record<string, any>
|
||||||
messages: Array<IAMessage>
|
messages: Array<IAMessage>
|
||||||
onSendMessage: (message: string, campoId?: string) => void
|
onSendMessage: (message: string, campoId?: string) => void
|
||||||
onAcceptSuggestion: (sugerencia: IASugerencia) => void
|
onAcceptSuggestion: (sugerencia: IASugerencia) => void
|
||||||
@@ -72,7 +72,7 @@ interface IAAsignaturaTabProps {
|
|||||||
|
|
||||||
export function IAAsignaturaTab({
|
export function IAAsignaturaTab({
|
||||||
campos,
|
campos,
|
||||||
datosGenerales,
|
asignatura: datosGenerales,
|
||||||
messages,
|
messages,
|
||||||
onSendMessage,
|
onSendMessage,
|
||||||
onAcceptSuggestion,
|
onAcceptSuggestion,
|
||||||
|
|||||||
@@ -7,7 +7,11 @@ import type { DocumentoResult } from './plans.api'
|
|||||||
import type {
|
import type {
|
||||||
Asignatura,
|
Asignatura,
|
||||||
BibliografiaAsignatura,
|
BibliografiaAsignatura,
|
||||||
|
CarreraRow,
|
||||||
CambioAsignatura,
|
CambioAsignatura,
|
||||||
|
EstructuraAsignatura,
|
||||||
|
FacultadRow,
|
||||||
|
PlanEstudioRow,
|
||||||
TipoAsignatura,
|
TipoAsignatura,
|
||||||
UUID,
|
UUID,
|
||||||
} from '../types/domain'
|
} from '../types/domain'
|
||||||
@@ -34,7 +38,55 @@ const EDGE = {
|
|||||||
subjects_get_document: 'subjects_get_document',
|
subjects_get_document: 'subjects_get_document',
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
export async function subjects_get(subjectId: UUID): Promise<Asignatura> {
|
export type FacultadInSubject = Pick<
|
||||||
|
FacultadRow,
|
||||||
|
'id' | 'nombre' | 'nombre_corto' | 'color' | 'icono'
|
||||||
|
>
|
||||||
|
|
||||||
|
export type CarreraInSubject = Pick<
|
||||||
|
CarreraRow,
|
||||||
|
'id' | 'facultad_id' | 'nombre' | 'nombre_corto' | 'clave_sep' | 'activa'
|
||||||
|
> & {
|
||||||
|
facultades: FacultadInSubject | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PlanEstudioInSubject = Pick<
|
||||||
|
PlanEstudioRow,
|
||||||
|
| 'id'
|
||||||
|
| 'carrera_id'
|
||||||
|
| 'estructura_id'
|
||||||
|
| 'nombre'
|
||||||
|
| 'nivel'
|
||||||
|
| 'tipo_ciclo'
|
||||||
|
| 'numero_ciclos'
|
||||||
|
| 'datos'
|
||||||
|
| 'estado_actual_id'
|
||||||
|
| 'activo'
|
||||||
|
| 'tipo_origen'
|
||||||
|
| 'meta_origen'
|
||||||
|
| 'creado_por'
|
||||||
|
| 'actualizado_por'
|
||||||
|
| 'creado_en'
|
||||||
|
| 'actualizado_en'
|
||||||
|
> & {
|
||||||
|
carreras: CarreraInSubject | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export type EstructuraAsignaturaInSubject = Pick<
|
||||||
|
EstructuraAsignatura,
|
||||||
|
'id' | 'nombre' | 'version' | 'definicion'
|
||||||
|
>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tipo real que devuelve `subjects_get` (asignatura + relaciones seleccionadas).
|
||||||
|
* Nota: `asignaturas_update` (update directo) NO devuelve estas relaciones.
|
||||||
|
*/
|
||||||
|
export type AsignaturaDetail = Asignatura & {
|
||||||
|
planes_estudio: PlanEstudioInSubject | null
|
||||||
|
estructuras_asignatura: EstructuraAsignaturaInSubject | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function subjects_get(subjectId: UUID): Promise<AsignaturaDetail> {
|
||||||
const supabase = supabaseBrowser()
|
const supabase = supabaseBrowser()
|
||||||
|
|
||||||
const { data, error } = await supabase
|
const { data, error } = await supabase
|
||||||
|
|||||||
@@ -97,7 +97,6 @@ export function useCreateSubjectManual() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useGenerateSubjectAI() {
|
export function useGenerateSubjectAI() {
|
||||||
const qc = useQueryClient()
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: ai_generate_subject,
|
mutationFn: ai_generate_subject,
|
||||||
})
|
})
|
||||||
@@ -162,7 +161,9 @@ export function useUpdateSubjectFields() {
|
|||||||
mutationFn: (vars: { subjectId: UUID; patch: SubjectsUpdateFieldsPatch }) =>
|
mutationFn: (vars: { subjectId: UUID; patch: SubjectsUpdateFieldsPatch }) =>
|
||||||
subjects_update_fields(vars.subjectId, vars.patch),
|
subjects_update_fields(vars.subjectId, vars.patch),
|
||||||
onSuccess: (updated) => {
|
onSuccess: (updated) => {
|
||||||
qc.setQueryData(qk.asignatura(updated.id), updated)
|
qc.setQueryData(qk.asignatura(updated.id), (prev) =>
|
||||||
|
prev ? { ...(prev as any), ...(updated as any) } : updated,
|
||||||
|
)
|
||||||
qc.invalidateQueries({
|
qc.invalidateQueries({
|
||||||
queryKey: qk.planAsignaturas(updated.plan_estudio_id),
|
queryKey: qk.planAsignaturas(updated.plan_estudio_id),
|
||||||
})
|
})
|
||||||
@@ -178,7 +179,9 @@ export function useUpdateSubjectContenido() {
|
|||||||
mutationFn: (vars: { subjectId: UUID; unidades: Array<any> }) =>
|
mutationFn: (vars: { subjectId: UUID; unidades: Array<any> }) =>
|
||||||
subjects_update_contenido(vars.subjectId, vars.unidades),
|
subjects_update_contenido(vars.subjectId, vars.unidades),
|
||||||
onSuccess: (updated) => {
|
onSuccess: (updated) => {
|
||||||
qc.setQueryData(qk.asignatura(updated.id), updated)
|
qc.setQueryData(qk.asignatura(updated.id), (prev) =>
|
||||||
|
prev ? { ...(prev as any), ...(updated as any) } : updated,
|
||||||
|
)
|
||||||
qc.invalidateQueries({ queryKey: qk.asignaturaHistorial(updated.id) })
|
qc.invalidateQueries({ queryKey: qk.asignaturaHistorial(updated.id) })
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -221,17 +224,22 @@ export function useUpdateAsignatura() {
|
|||||||
}) => asignaturas_update(vars.asignaturaId, vars.patch),
|
}) => asignaturas_update(vars.asignaturaId, vars.patch),
|
||||||
|
|
||||||
onSuccess: (updated) => {
|
onSuccess: (updated) => {
|
||||||
// 1. Actualizamos la materia específica en la caché si tienes un query de "detalle"
|
// ✅ Mantener consistencia con las query keys centralizadas (qk)
|
||||||
qc.setQueryData(['asignatura', updated.id], updated)
|
// 1) Actualiza el detalle (esto evita volver a entrar con caché vieja)
|
||||||
|
qc.setQueryData(qk.asignatura(updated.id), (prev) =>
|
||||||
|
prev ? { ...(prev as any), ...(updated as any) } : updated,
|
||||||
|
)
|
||||||
|
|
||||||
// 2. IMPORTANTÍSIMO: Invalidamos la lista de materias del plan
|
// 2) Refresca vistas derivadas del plan
|
||||||
// para que el mapa curricular vea los cambios (créditos, horas, nombre, etc.)
|
|
||||||
qc.invalidateQueries({
|
qc.invalidateQueries({
|
||||||
queryKey: ['plan_asignaturas', updated.plan_estudio_id],
|
queryKey: qk.planAsignaturas(updated.plan_estudio_id),
|
||||||
|
})
|
||||||
|
qc.invalidateQueries({
|
||||||
|
queryKey: qk.planHistorial(updated.plan_estudio_id),
|
||||||
})
|
})
|
||||||
|
|
||||||
// 3. Si tienes una lista general de asignaturas, también la invalidamos
|
// 3) Refresca historial de la asignatura si existe
|
||||||
qc.invalidateQueries({ queryKey: ['asignaturas', 'list'] })
|
qc.invalidateQueries({ queryKey: qk.asignaturaHistorial(updated.id) })
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ function AsignaturasPage() {
|
|||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
// 1. Fetch de datos reales
|
// 1. Fetch de datos reales
|
||||||
const { data: asignaturasApi, isLoading: loadingAsig } =
|
const { data: asignaturaApi, isLoading: loadingAsig } =
|
||||||
usePlanAsignaturas(planId)
|
usePlanAsignaturas(planId)
|
||||||
const { data: lineasApi, isLoading: loadingLineas } = usePlanLineas(planId)
|
const { data: lineasApi, isLoading: loadingLineas } = usePlanLineas(planId)
|
||||||
|
|
||||||
@@ -99,8 +99,8 @@ function AsignaturasPage() {
|
|||||||
|
|
||||||
// 3. Procesamiento de datos
|
// 3. Procesamiento de datos
|
||||||
const asignaturas = useMemo(
|
const asignaturas = useMemo(
|
||||||
() => mapAsignaturas(asignaturasApi),
|
() => mapAsignaturas(asignaturaApi),
|
||||||
[asignaturasApi],
|
[asignaturaApi],
|
||||||
)
|
)
|
||||||
const lineas = useMemo(() => lineasApi || [], [lineasApi])
|
const lineas = useMemo(() => lineasApi || [], [lineasApi])
|
||||||
|
|
||||||
|
|||||||
@@ -183,7 +183,7 @@ function MapaCurricularPage() {
|
|||||||
const { mutate: createLinea } = useCreateLinea()
|
const { mutate: createLinea } = useCreateLinea()
|
||||||
const { mutate: updateLineaApi } = useUpdateLinea()
|
const { mutate: updateLineaApi } = useUpdateLinea()
|
||||||
const { mutate: deleteLineaApi } = useDeleteLinea()
|
const { mutate: deleteLineaApi } = useDeleteLinea()
|
||||||
const { data: asignaturasApi, isLoading: loadingAsig } =
|
const { data: asignaturaApi, isLoading: loadingAsig } =
|
||||||
usePlanAsignaturas(planId)
|
usePlanAsignaturas(planId)
|
||||||
const { data: lineasApi, isLoading: loadingLineas } = usePlanLineas(planId)
|
const { data: lineasApi, isLoading: loadingLineas } = usePlanLineas(planId)
|
||||||
const [asignaturas, setAsignaturas] = useState<Array<Asignatura>>([])
|
const [asignaturas, setAsignaturas] = useState<Array<Asignatura>>([])
|
||||||
@@ -286,9 +286,9 @@ function MapaCurricularPage() {
|
|||||||
}, [lineas])
|
}, [lineas])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (asignaturasApi)
|
if (asignaturaApi)
|
||||||
setAsignaturas(mapAsignaturasToAsignaturas(asignaturasApi))
|
setAsignaturas(mapAsignaturasToAsignaturas(asignaturaApi))
|
||||||
}, [asignaturasApi])
|
}, [asignaturaApi])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (lineasApi) setLineas(mapLineasToLineaCurricular(lineasApi))
|
if (lineasApi) setLineas(mapLineasToLineaCurricular(lineasApi))
|
||||||
|
|||||||
Reference in New Issue
Block a user