Ahora hay persistencia en la asignatura #118
@@ -366,7 +366,8 @@ export default function AsignaturaDetailPage() {
|
|||||||
|
|
||||||
<TabsContent value="contenido">
|
<TabsContent value="contenido">
|
||||||
<ContenidoTematico
|
<ContenidoTematico
|
||||||
data={asignaturaApi}
|
asignaturaId={asignaturaId}
|
||||||
|
data={asignaturaApi ?? null}
|
||||||
isLoading={loadingAsig}
|
isLoading={loadingAsig}
|
||||||
></ContenidoTematico>
|
></ContenidoTematico>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { useEffect, useState } from 'react'
|
|
||||||
import {
|
import {
|
||||||
Plus,
|
Plus,
|
||||||
GripVertical,
|
GripVertical,
|
||||||
@@ -7,17 +6,11 @@ import {
|
|||||||
Edit3,
|
Edit3,
|
||||||
Trash2,
|
Trash2,
|
||||||
Clock,
|
Clock,
|
||||||
Save,
|
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
import { useEffect, useState } from 'react'
|
||||||
import { Button } from '@/components/ui/button'
|
|
||||||
import { Input } from '@/components/ui/input'
|
import type { ContenidoApi, ContenidoTemaApi } from '@/data/api/subjects.api'
|
||||||
import { Badge } from '@/components/ui/badge'
|
|
||||||
import {
|
|
||||||
Collapsible,
|
|
||||||
CollapsibleContent,
|
|
||||||
CollapsibleTrigger,
|
|
||||||
} from '@/components/ui/collapsible'
|
|
||||||
import {
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
AlertDialogAction,
|
AlertDialogAction,
|
||||||
@@ -28,8 +21,18 @@ import {
|
|||||||
AlertDialogHeader,
|
AlertDialogHeader,
|
||||||
AlertDialogTitle,
|
AlertDialogTitle,
|
||||||
} from '@/components/ui/alert-dialog'
|
} from '@/components/ui/alert-dialog'
|
||||||
|
import { Badge } from '@/components/ui/badge'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
|
import {
|
||||||
|
Collapsible,
|
||||||
|
CollapsibleContent,
|
||||||
|
CollapsibleTrigger,
|
||||||
|
} from '@/components/ui/collapsible'
|
||||||
|
import { Input } from '@/components/ui/input'
|
||||||
|
import { useUpdateSubjectContenido } from '@/data/hooks/useSubjects'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
//import { toast } from 'sonner';
|
// import { toast } from 'sonner';
|
||||||
|
|
||||||
export interface Tema {
|
export interface Tema {
|
||||||
id: string
|
id: string
|
||||||
@@ -42,41 +45,133 @@ export interface UnidadTematica {
|
|||||||
id: string
|
id: string
|
||||||
nombre: string
|
nombre: string
|
||||||
numero: number
|
numero: number
|
||||||
temas: Tema[]
|
temas: Array<Tema>
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialData: UnidadTematica[] = [
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||||
{
|
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
||||||
id: 'u1',
|
}
|
||||||
numero: 1,
|
|
||||||
nombre: 'Fundamentos de Inteligencia Artificial',
|
|
||||||
temas: [
|
|
||||||
{ id: 't1', nombre: 'Tipos de IA y aplicaciones', horasEstimadas: 6 },
|
|
||||||
{ id: 't2', nombre: 'Ética en IA', horasEstimadas: 3 },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
// Estructura que viene de tu JSON/API
|
function coerceNumber(value: unknown): number | undefined {
|
||||||
interface ContenidoApi {
|
if (typeof value === 'number' && Number.isFinite(value)) return value
|
||||||
unidad: number
|
if (typeof value === 'string') {
|
||||||
titulo: string
|
const trimmed = value.trim()
|
||||||
temas: string[] | any[] // Acepta strings o objetos
|
if (!trimmed) return undefined
|
||||||
[key: string]: any // Esta línea permite que haya más claves desconocidas
|
const parsed = Number(trimmed)
|
||||||
|
return Number.isFinite(parsed) ? parsed : undefined
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
function coerceString(value: unknown): string | undefined {
|
||||||
|
if (typeof value === 'string') return value
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapTemaValue(value: unknown): ContenidoTemaApi | null {
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
const trimmed = value.trim()
|
||||||
|
return trimmed ? trimmed : null
|
||||||
|
}
|
||||||
|
if (isRecord(value)) {
|
||||||
|
const nombre = coerceString(value.nombre)
|
||||||
|
if (!nombre) return null
|
||||||
|
const horasEstimadas = coerceNumber(value.horasEstimadas)
|
||||||
|
const descripcion = coerceString(value.descripcion)
|
||||||
|
return {
|
||||||
|
...value,
|
||||||
|
nombre,
|
||||||
|
horasEstimadas,
|
||||||
|
descripcion,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapContenidoItem(value: unknown, index: number): ContenidoApi | null {
|
||||||
|
if (!isRecord(value)) return null
|
||||||
|
|
||||||
|
const unidad = coerceNumber(value.unidad) ?? index + 1
|
||||||
|
const titulo = coerceString(value.titulo) ?? 'Sin título'
|
||||||
|
|
||||||
|
let temas: Array<ContenidoTemaApi> = []
|
||||||
|
if (Array.isArray(value.temas)) {
|
||||||
|
temas = value.temas
|
||||||
|
.map(mapTemaValue)
|
||||||
|
.filter((t): t is ContenidoTemaApi => t !== null)
|
||||||
|
} else if (typeof value.temas === 'string' && value.temas.trim()) {
|
||||||
|
temas = value.temas
|
||||||
|
.split(/\r?\n|,/)
|
||||||
|
.map((t) => t.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
}
|
||||||
|
|
||||||
|
return { unidad, titulo, temas }
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapContenidoTematicoFromDb(value: unknown): Array<ContenidoApi> {
|
||||||
|
if (value == null) return []
|
||||||
|
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
try {
|
||||||
|
return mapContenidoTematicoFromDb(JSON.parse(value))
|
||||||
|
} catch {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value
|
||||||
|
.map((item, idx) => mapContenidoItem(item, idx))
|
||||||
|
.filter((x): x is ContenidoApi => x !== null)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isRecord(value)) {
|
||||||
|
if (Array.isArray(value.contenido_tematico)) {
|
||||||
|
return mapContenidoTematicoFromDb(value.contenido_tematico)
|
||||||
|
}
|
||||||
|
if (Array.isArray(value.unidades)) {
|
||||||
|
return mapContenidoTematicoFromDb(value.unidades)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
function serializeUnidadesToApi(
|
||||||
|
unidades: Array<UnidadTematica>,
|
||||||
|
): Array<ContenidoApi> {
|
||||||
|
return unidades
|
||||||
|
.slice()
|
||||||
|
.sort((a, b) => a.numero - b.numero)
|
||||||
|
.map((u, idx) => ({
|
||||||
|
unidad: u.numero || idx + 1,
|
||||||
|
titulo: u.nombre || 'Sin título',
|
||||||
|
temas: u.temas.map((t) => ({
|
||||||
|
nombre: t.nombre || 'Tema',
|
||||||
|
horasEstimadas: t.horasEstimadas ?? 0,
|
||||||
|
descripcion: t.descripcion,
|
||||||
|
})),
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Props del componente
|
// Props del componente
|
||||||
interface ContenidoTematicoProps {
|
interface ContenidoTematicoProps {
|
||||||
data: {
|
asignaturaId: string
|
||||||
contenido_tematico: ContenidoApi[]
|
data?: {
|
||||||
}
|
contenido_tematico?: unknown
|
||||||
|
} | null
|
||||||
isLoading: boolean
|
isLoading: boolean
|
||||||
}
|
}
|
||||||
export function ContenidoTematico({ data, isLoading }: ContenidoTematicoProps) {
|
export function ContenidoTematico({
|
||||||
const [unidades, setUnidades] = useState<UnidadTematica[]>([])
|
asignaturaId,
|
||||||
const [expandedUnits, setExpandedUnits] = useState<Set<string>>(
|
data,
|
||||||
new Set(['u1']),
|
isLoading,
|
||||||
)
|
}: ContenidoTematicoProps) {
|
||||||
|
const updateContenido = useUpdateSubjectContenido()
|
||||||
|
|
||||||
|
const [unidades, setUnidades] = useState<Array<UnidadTematica>>([])
|
||||||
|
const [expandedUnits, setExpandedUnits] = useState<Set<string>>(new Set())
|
||||||
const [deleteDialog, setDeleteDialog] = useState<{
|
const [deleteDialog, setDeleteDialog] = useState<{
|
||||||
type: 'unidad' | 'tema'
|
type: 'unidad' | 'tema'
|
||||||
id: string
|
id: string
|
||||||
@@ -87,30 +182,40 @@ export function ContenidoTematico({ data, isLoading }: ContenidoTematicoProps) {
|
|||||||
unitId: string
|
unitId: string
|
||||||
temaId: string
|
temaId: string
|
||||||
} | null>(null)
|
} | null>(null)
|
||||||
const [isSaving, setIsSaving] = useState(false)
|
|
||||||
|
const persistUnidades = async (nextUnidades: Array<UnidadTematica>) => {
|
||||||
|
const payload = serializeUnidadesToApi(nextUnidades)
|
||||||
|
await updateContenido.mutateAsync({
|
||||||
|
subjectId: asignaturaId,
|
||||||
|
unidades: payload,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data?.contenido_tematico) {
|
const contenido = mapContenidoTematicoFromDb(
|
||||||
const transformed = data.contenido_tematico.map(
|
data ? data.contenido_tematico : undefined,
|
||||||
(u: any, idx: number) => ({
|
)
|
||||||
id: `u-${idx}`,
|
|
||||||
numero: u.unidad || idx + 1,
|
|
||||||
nombre: u.titulo || 'Sin título',
|
|
||||||
temas: Array.isArray(u.temas)
|
|
||||||
? u.temas.map((t: any, tidx: number) => ({
|
|
||||||
id: `t-${idx}-${tidx}`,
|
|
||||||
nombre: typeof t === 'string' ? t : t.nombre || 'Tema',
|
|
||||||
horasEstimadas: t.horasEstimadas || 0,
|
|
||||||
}))
|
|
||||||
: [],
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
setUnidades(transformed)
|
|
||||||
|
|
||||||
// Expandir la primera unidad automáticamente
|
const transformed = contenido.map((u, idx) => ({
|
||||||
if (transformed.length > 0) {
|
id: `u-${idx}`,
|
||||||
setExpandedUnits(new Set([transformed[0].id]))
|
numero: u.unidad || idx + 1,
|
||||||
}
|
nombre: u.titulo || 'Sin título',
|
||||||
|
temas: Array.isArray(u.temas)
|
||||||
|
? u.temas.map((t: any, tidx: number) => ({
|
||||||
|
id: `t-${idx}-${tidx}`,
|
||||||
|
nombre: typeof t === 'string' ? t : t?.nombre || 'Tema',
|
||||||
|
horasEstimadas: t?.horasEstimadas || 0,
|
||||||
|
}))
|
||||||
|
: [],
|
||||||
|
}))
|
||||||
|
|
||||||
|
setUnidades(transformed)
|
||||||
|
|
||||||
|
// Expandir la primera unidad automáticamente
|
||||||
|
if (transformed.length > 0) {
|
||||||
|
setExpandedUnits(new Set([transformed[0].id]))
|
||||||
|
} else {
|
||||||
|
setExpandedUnits(new Set())
|
||||||
}
|
}
|
||||||
}, [data])
|
}, [data])
|
||||||
|
|
||||||
@@ -139,7 +244,8 @@ export function ContenidoTematico({ data, isLoading }: ContenidoTematicoProps) {
|
|||||||
numero: unidades.length + 1,
|
numero: unidades.length + 1,
|
||||||
temas: [],
|
temas: [],
|
||||||
}
|
}
|
||||||
setUnidades([...unidades, newUnidad])
|
const next = [...unidades, newUnidad]
|
||||||
|
setUnidades(next)
|
||||||
setExpandedUnits(new Set([...expandedUnits, newId]))
|
setExpandedUnits(new Set([...expandedUnits, newId]))
|
||||||
setEditingUnit(newId)
|
setEditingUnit(newId)
|
||||||
}
|
}
|
||||||
@@ -189,23 +295,22 @@ export function ContenidoTematico({ data, isLoading }: ContenidoTematicoProps) {
|
|||||||
|
|
||||||
const handleDelete = () => {
|
const handleDelete = () => {
|
||||||
if (!deleteDialog) return
|
if (!deleteDialog) return
|
||||||
|
let next: Array<UnidadTematica> = unidades
|
||||||
if (deleteDialog.type === 'unidad') {
|
if (deleteDialog.type === 'unidad') {
|
||||||
setUnidades(
|
next = unidades
|
||||||
unidades
|
.filter((u) => u.id !== deleteDialog.id)
|
||||||
.filter((u) => u.id !== deleteDialog.id)
|
.map((u, i) => ({ ...u, numero: i + 1 }))
|
||||||
.map((u, i) => ({ ...u, numero: i + 1 })),
|
|
||||||
)
|
|
||||||
} else if (deleteDialog.parentId) {
|
} else if (deleteDialog.parentId) {
|
||||||
setUnidades(
|
next = unidades.map((u) =>
|
||||||
unidades.map((u) =>
|
u.id === deleteDialog.parentId
|
||||||
u.id === deleteDialog.parentId
|
? { ...u, temas: u.temas.filter((t) => t.id !== deleteDialog.id) }
|
||||||
? { ...u, temas: u.temas.filter((t) => t.id !== deleteDialog.id) }
|
: u,
|
||||||
: u,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
setUnidades(next)
|
||||||
setDeleteDialog(null)
|
setDeleteDialog(null)
|
||||||
//toast.success("Eliminado correctamente");
|
void persistUnidades(next)
|
||||||
|
// toast.success("Eliminado correctamente");
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -223,19 +328,6 @@ export function ContenidoTematico({ data, isLoading }: ContenidoTematicoProps) {
|
|||||||
<Button variant="outline" onClick={addUnidad} className="gap-2">
|
<Button variant="outline" onClick={addUnidad} className="gap-2">
|
||||||
<Plus className="h-4 w-4" /> Nueva unidad
|
<Plus className="h-4 w-4" /> Nueva unidad
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
setIsSaving(true)
|
|
||||||
setTimeout(() => {
|
|
||||||
setIsSaving(false) /*toast.success("Guardado")*/
|
|
||||||
}, 1000)
|
|
||||||
}}
|
|
||||||
disabled={isSaving}
|
|
||||||
className="bg-blue-600 hover:bg-blue-700"
|
|
||||||
>
|
|
||||||
<Save className="mr-2 h-4 w-4" />{' '}
|
|
||||||
{isSaving ? 'Guardando...' : 'Guardar'}
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -271,12 +363,17 @@ export function ContenidoTematico({ data, isLoading }: ContenidoTematicoProps) {
|
|||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
updateUnidadNombre(unidad.id, e.target.value)
|
updateUnidadNombre(unidad.id, e.target.value)
|
||||||
}
|
}
|
||||||
onBlur={() => setEditingUnit(null)}
|
onBlur={() => {
|
||||||
onKeyDown={(e) =>
|
setEditingUnit(null)
|
||||||
e.key === 'Enter' && setEditingUnit(null)
|
void persistUnidades(unidades)
|
||||||
}
|
}}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
setEditingUnit(null)
|
||||||
|
void persistUnidades(unidades)
|
||||||
|
}
|
||||||
|
}}
|
||||||
className="h-8 max-w-md bg-white"
|
className="h-8 max-w-md bg-white"
|
||||||
autoFocus
|
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<CardTitle
|
<CardTitle
|
||||||
@@ -318,13 +415,17 @@ export function ContenidoTematico({ data, isLoading }: ContenidoTematicoProps) {
|
|||||||
tema={tema}
|
tema={tema}
|
||||||
index={idx + 1}
|
index={idx + 1}
|
||||||
isEditing={
|
isEditing={
|
||||||
editingTema?.unitId === unidad.id &&
|
!!editingTema &&
|
||||||
editingTema?.temaId === tema.id
|
editingTema.unitId === unidad.id &&
|
||||||
|
editingTema.temaId === tema.id
|
||||||
}
|
}
|
||||||
onEdit={() =>
|
onEdit={() =>
|
||||||
setEditingTema({ unitId: unidad.id, temaId: tema.id })
|
setEditingTema({ unitId: unidad.id, temaId: tema.id })
|
||||||
}
|
}
|
||||||
onStopEditing={() => setEditingTema(null)}
|
onStopEditing={() => {
|
||||||
|
setEditingTema(null)
|
||||||
|
void persistUnidades(unidades)
|
||||||
|
}}
|
||||||
onUpdate={(updates) =>
|
onUpdate={(updates) =>
|
||||||
updateTema(unidad.id, tema.id, updates)
|
updateTema(unidad.id, tema.id, updates)
|
||||||
}
|
}
|
||||||
@@ -397,7 +498,6 @@ function TemaRow({
|
|||||||
onChange={(e) => onUpdate({ nombre: e.target.value })}
|
onChange={(e) => onUpdate({ nombre: e.target.value })}
|
||||||
className="h-8 flex-1 bg-white"
|
className="h-8 flex-1 bg-white"
|
||||||
placeholder="Nombre"
|
placeholder="Nombre"
|
||||||
autoFocus
|
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
@@ -417,9 +517,13 @@ function TemaRow({
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div className="flex-1 cursor-pointer" onClick={onEdit}>
|
<button
|
||||||
|
type="button"
|
||||||
|
className="flex-1 cursor-pointer text-left"
|
||||||
|
onClick={onEdit}
|
||||||
|
>
|
||||||
<p className="text-sm font-medium text-slate-700">{tema.nombre}</p>
|
<p className="text-sm font-medium text-slate-700">{tema.nombre}</p>
|
||||||
</div>
|
</button>
|
||||||
<Badge variant="secondary" className="text-[10px] opacity-60">
|
<Badge variant="secondary" className="text-[10px] opacity-60">
|
||||||
{tema.horasEstimadas}h
|
{tema.horasEstimadas}h
|
||||||
</Badge>
|
</Badge>
|
||||||
|
|||||||
@@ -31,13 +31,32 @@ const EDGE = {
|
|||||||
subjects_import_from_file: 'subjects_import_from_file',
|
subjects_import_from_file: 'subjects_import_from_file',
|
||||||
|
|
||||||
subjects_update_fields: 'subjects_update_fields',
|
subjects_update_fields: 'subjects_update_fields',
|
||||||
subjects_update_contenido: 'subjects_update_contenido',
|
|
||||||
subjects_update_bibliografia: 'subjects_update_bibliografia',
|
subjects_update_bibliografia: 'subjects_update_bibliografia',
|
||||||
|
|
||||||
subjects_generate_document: 'subjects_generate_document',
|
subjects_generate_document: 'subjects_generate_document',
|
||||||
subjects_get_document: 'subjects_get_document',
|
subjects_get_document: 'subjects_get_document',
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
|
export type ContenidoTemaApi =
|
||||||
|
| string
|
||||||
|
| {
|
||||||
|
nombre: string
|
||||||
|
horasEstimadas?: number
|
||||||
|
descripcion?: string
|
||||||
|
[key: string]: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Estructura persistida en `asignaturas.contenido_tematico`.
|
||||||
|
* La BDD guarda un arreglo de unidades, cada una con temas (strings u objetos).
|
||||||
|
*/
|
||||||
|
export type ContenidoApi = {
|
||||||
|
unidad: number
|
||||||
|
titulo: string
|
||||||
|
temas: Array<ContenidoTemaApi>
|
||||||
|
[key: string]: unknown
|
||||||
|
}
|
||||||
|
|
||||||
export type FacultadInSubject = Pick<
|
export type FacultadInSubject = Pick<
|
||||||
FacultadRow,
|
FacultadRow,
|
||||||
'id' | 'nombre' | 'nombre_corto' | 'color' | 'icono'
|
'id' | 'nombre' | 'nombre_corto' | 'color' | 'icono'
|
||||||
@@ -81,7 +100,8 @@ export type EstructuraAsignaturaInSubject = Pick<
|
|||||||
* Tipo real que devuelve `subjects_get` (asignatura + relaciones seleccionadas).
|
* Tipo real que devuelve `subjects_get` (asignatura + relaciones seleccionadas).
|
||||||
* Nota: `asignaturas_update` (update directo) NO devuelve estas relaciones.
|
* Nota: `asignaturas_update` (update directo) NO devuelve estas relaciones.
|
||||||
*/
|
*/
|
||||||
export type AsignaturaDetail = Asignatura & {
|
export type AsignaturaDetail = Omit<Asignatura, 'contenido_tematico'> & {
|
||||||
|
contenido_tematico: Array<ContenidoApi> | null
|
||||||
planes_estudio: PlanEstudioInSubject | null
|
planes_estudio: PlanEstudioInSubject | null
|
||||||
estructuras_asignatura: EstructuraAsignaturaInSubject | null
|
estructuras_asignatura: EstructuraAsignaturaInSubject | null
|
||||||
}
|
}
|
||||||
@@ -105,7 +125,10 @@ export async function subjects_get(subjectId: UUID): Promise<AsignaturaDetail> {
|
|||||||
.single()
|
.single()
|
||||||
|
|
||||||
throwIfError(error)
|
throwIfError(error)
|
||||||
return requireData(data, 'Asignatura no encontrada.')
|
return requireData(
|
||||||
|
data,
|
||||||
|
'Asignatura no encontrada.',
|
||||||
|
) as unknown as AsignaturaDetail
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function subjects_history(
|
export async function subjects_history(
|
||||||
@@ -323,12 +346,24 @@ export async function subjects_update_fields(
|
|||||||
|
|
||||||
export async function subjects_update_contenido(
|
export async function subjects_update_contenido(
|
||||||
subjectId: UUID,
|
subjectId: UUID,
|
||||||
unidades: Array<any>,
|
unidades: Array<ContenidoApi>,
|
||||||
): Promise<Asignatura> {
|
): Promise<Asignatura> {
|
||||||
return invokeEdge<Asignatura>(EDGE.subjects_update_contenido, {
|
const supabase = supabaseBrowser()
|
||||||
subjectId,
|
|
||||||
unidades,
|
type AsignaturaUpdate = Database['public']['Tables']['asignaturas']['Update']
|
||||||
})
|
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from('asignaturas')
|
||||||
|
.update({
|
||||||
|
contenido_tematico:
|
||||||
|
unidades as unknown as AsignaturaUpdate['contenido_tematico'],
|
||||||
|
})
|
||||||
|
.eq('id', subjectId)
|
||||||
|
.select()
|
||||||
|
.single()
|
||||||
|
|
||||||
|
throwIfError(error)
|
||||||
|
return requireData(data, 'No se pudo actualizar la asignatura.')
|
||||||
}
|
}
|
||||||
|
|
||||||
export type BibliografiaUpsertInput = Array<{
|
export type BibliografiaUpsertInput = Array<{
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import { qk } from '../query/keys'
|
|||||||
|
|
||||||
import type {
|
import type {
|
||||||
BibliografiaUpsertInput,
|
BibliografiaUpsertInput,
|
||||||
|
ContenidoApi,
|
||||||
SubjectsUpdateFieldsPatch,
|
SubjectsUpdateFieldsPatch,
|
||||||
} from '../api/subjects.api'
|
} from '../api/subjects.api'
|
||||||
import type { UUID } from '../types/domain'
|
import type { UUID } from '../types/domain'
|
||||||
@@ -176,12 +177,19 @@ export function useUpdateSubjectContenido() {
|
|||||||
const qc = useQueryClient()
|
const qc = useQueryClient()
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: (vars: { subjectId: UUID; unidades: Array<any> }) =>
|
mutationFn: (vars: { subjectId: UUID; unidades: Array<ContenidoApi> }) =>
|
||||||
subjects_update_contenido(vars.subjectId, vars.unidades),
|
subjects_update_contenido(vars.subjectId, vars.unidades),
|
||||||
onSuccess: (updated) => {
|
onSuccess: (updated) => {
|
||||||
qc.setQueryData(qk.asignatura(updated.id), (prev) =>
|
qc.setQueryData(qk.asignatura(updated.id), (prev) =>
|
||||||
prev ? { ...(prev as any), ...(updated as any) } : updated,
|
prev ? { ...(prev as any), ...(updated as any) } : updated,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
qc.invalidateQueries({
|
||||||
|
queryKey: qk.planAsignaturas(updated.plan_estudio_id),
|
||||||
|
})
|
||||||
|
qc.invalidateQueries({
|
||||||
|
queryKey: qk.planHistorial(updated.plan_estudio_id),
|
||||||
|
})
|
||||||
qc.invalidateQueries({ queryKey: qk.asignaturaHistorial(updated.id) })
|
qc.invalidateQueries({ queryKey: qk.asignaturaHistorial(updated.id) })
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user