Se hidrata de informacion las tabs de asignatura

This commit is contained in:
2026-01-14 15:52:25 -06:00
parent c4329785cc
commit b4b5134cb2
9 changed files with 1490 additions and 819 deletions

View File

@@ -1,14 +1,44 @@
import { useState } from 'react';
import { Plus, Search, BookOpen, Trash2, Library, Edit3, Save } from 'lucide-react';
import { Card, CardContent } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
import { Badge } from '@/components/ui/badge';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from '@/components/ui/alert-dialog';
import { cn } from '@/lib/utils';
import { useEffect, useState } from 'react'
import {
Plus,
Search,
BookOpen,
Trash2,
Library,
Edit3,
Save,
} from 'lucide-react'
import { Card, CardContent } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Textarea } from '@/components/ui/textarea'
import { Badge } from '@/components/ui/badge'
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog'
import { cn } from '@/lib/utils'
import { useSubjectBibliografia } from '@/data/hooks/useSubjects'
//import { toast } from 'sonner';
//import { mockLibraryResources } from '@/data/mockMateriaData';
@@ -20,7 +50,7 @@ export const mockLibraryResources = [
editorial: 'MIT Press',
anio: 2016,
isbn: '9780262035613',
disponible: true
disponible: true,
},
{
id: 'lib-2',
@@ -29,102 +59,154 @@ export const mockLibraryResources = [
editorial: 'Pearson',
anio: 2020,
isbn: '9780134610993',
disponible: true
disponible: true,
},
{
id: 'lib-3',
titulo: 'Hands-On Machine Learning',
autor: 'Aurélien Géron',
editorial: 'O\'Reilly Media',
editorial: "O'Reilly Media",
anio: 2019,
isbn: '9781492032649',
disponible: false
}
];
disponible: false,
},
]
// --- Interfaces ---
export interface BibliografiaEntry {
id: string;
tipo: 'BASICA' | 'COMPLEMENTARIA';
cita: string;
fuenteBibliotecaId?: string;
fuenteBiblioteca?: any;
id: string
tipo: 'BASICA' | 'COMPLEMENTARIA'
cita: string
tipo_fuente?: 'MANUAL' | 'BIBLIOTECA'
biblioteca_item_id?: string | null
fuenteBibliotecaId?: string
fuenteBiblioteca?: any
}
interface BibliografiaTabProps {
bibliografia: BibliografiaEntry[];
onSave: (bibliografia: BibliografiaEntry[]) => void;
isSaving: boolean;
bibliografia: BibliografiaEntry[]
onSave: (bibliografia: BibliografiaEntry[]) => void
isSaving: boolean
}
export function BibliographyItem({ bibliografia, onSave, isSaving }: BibliografiaTabProps) {
const [entries, setEntries] = useState<BibliografiaEntry[]>(bibliografia);
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false);
const [isLibraryDialogOpen, setIsLibraryDialogOpen] = useState(false);
const [deleteId, setDeleteId] = useState<string | null>(null);
const [editingId, setEditingId] = useState<string | null>(null);
const [newEntryType, setNewEntryType] = useState<'BASICA' | 'COMPLEMENTARIA'>('BASICA');
export function BibliographyItem({
bibliografia,
onSave,
isSaving,
}: BibliografiaTabProps) {
const { data: bibliografia2, isLoading: loadinmateria } =
useSubjectBibliografia('9d4dda6a-488f-428a-8a07-38081592a641')
const [entries, setEntries] = useState<BibliografiaEntry[]>(bibliografia)
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false)
const [isLibraryDialogOpen, setIsLibraryDialogOpen] = useState(false)
const [deleteId, setDeleteId] = useState<string | null>(null)
const [editingId, setEditingId] = useState<string | null>(null)
const [newEntryType, setNewEntryType] = useState<'BASICA' | 'COMPLEMENTARIA'>(
'BASICA',
)
const basicaEntries = entries.filter(e => e.tipo === 'BASICA');
const complementariaEntries = entries.filter(e => e.tipo === 'COMPLEMENTARIA');
useEffect(() => {
if (bibliografia2 && Array.isArray(bibliografia2)) {
setEntries(bibliografia2)
} else if (bibliografia) {
// Fallback a la prop inicial si la API no devuelve nada
setEntries(bibliografia)
}
}, [bibliografia2, bibliografia])
const basicaEntries = entries.filter((e) => e.tipo === 'BASICA')
const complementariaEntries = entries.filter(
(e) => e.tipo === 'COMPLEMENTARIA',
)
console.log(bibliografia2)
const handleAddManual = (cita: string) => {
const newEntry: BibliografiaEntry = { id: `manual-${Date.now()}`, tipo: newEntryType, cita };
setEntries([...entries, newEntry]);
setIsAddDialogOpen(false);
const newEntry: BibliografiaEntry = {
id: `manual-${Date.now()}`,
tipo: newEntryType,
cita,
}
setEntries([...entries, newEntry])
setIsAddDialogOpen(false)
//toast.success('Referencia manual añadida');
};
}
const handleAddFromLibrary = (resource: any, tipo: 'BASICA' | 'COMPLEMENTARIA') => {
const cita = `${resource.autor} (${resource.anio}). ${resource.titulo}. ${resource.editorial}.`;
const handleAddFromLibrary = (
resource: any,
tipo: 'BASICA' | 'COMPLEMENTARIA',
) => {
const cita = `${resource.autor} (${resource.anio}). ${resource.titulo}. ${resource.editorial}.`
const newEntry: BibliografiaEntry = {
id: `lib-ref-${Date.now()}`,
tipo,
cita,
fuenteBibliotecaId: resource.id,
fuenteBiblioteca: resource,
};
setEntries([...entries, newEntry]);
setIsLibraryDialogOpen(false);
}
setEntries([...entries, newEntry])
setIsLibraryDialogOpen(false)
//toast.success('Añadido desde biblioteca');
};
}
const handleUpdateCita = (id: string, cita: string) => {
setEntries(entries.map(e => e.id === id ? { ...e, cita } : e));
};
setEntries(entries.map((e) => (e.id === id ? { ...e, cita } : e)))
}
return (
<div className="max-w-5xl mx-auto py-10 space-y-8 animate-in fade-in duration-500">
<div className="animate-in fade-in mx-auto max-w-5xl space-y-8 py-10 duration-500">
<div className="flex items-center justify-between border-b pb-4">
<div>
<h2 className="text-2xl font-bold text-slate-900 tracking-tight">Bibliografía</h2>
<p className="text-sm text-slate-500 mt-1">
{basicaEntries.length} básica {complementariaEntries.length} complementaria
<h2 className="text-2xl font-bold tracking-tight text-slate-900">
Bibliografía
</h2>
<p className="mt-1 text-sm text-slate-500">
{basicaEntries.length} básica {complementariaEntries.length}{' '}
complementaria
</p>
</div>
<div className="flex items-center gap-2">
<Dialog open={isLibraryDialogOpen} onOpenChange={setIsLibraryDialogOpen}>
<Dialog
open={isLibraryDialogOpen}
onOpenChange={setIsLibraryDialogOpen}
>
<DialogTrigger asChild>
<Button variant="outline" className="border-blue-200 text-blue-700 hover:bg-blue-50">
<Library className="w-4 h-4 mr-2" /> Buscar en biblioteca
<Button
variant="outline"
className="border-blue-200 text-blue-700 hover:bg-blue-50"
>
<Library className="mr-2 h-4 w-4" /> Buscar en biblioteca
</Button>
</DialogTrigger>
<DialogContent className="max-w-2xl">
<LibrarySearchDialog onSelect={handleAddFromLibrary} existingIds={entries.map(e => e.fuenteBibliotecaId || '')} />
<LibrarySearchDialog
onSelect={handleAddFromLibrary}
existingIds={entries.map((e) => e.fuenteBibliotecaId || '')}
/>
</DialogContent>
</Dialog>
<Dialog open={isAddDialogOpen} onOpenChange={setIsAddDialogOpen}>
<DialogTrigger asChild>
<Button variant="outline"><Plus className="w-4 h-4 mr-2" /> Añadir manual</Button>
<Button variant="outline">
<Plus className="mr-2 h-4 w-4" /> Añadir manual
</Button>
</DialogTrigger>
<DialogContent>
<AddManualDialog tipo={newEntryType} onTypeChange={setNewEntryType} onAdd={handleAddManual} />
<AddManualDialog
tipo={newEntryType}
onTypeChange={setNewEntryType}
onAdd={handleAddManual}
/>
</DialogContent>
</Dialog>
<Button onClick={() => onSave(entries)} disabled={isSaving} className="bg-blue-600 hover:bg-blue-700">
<Save className="w-4 h-4 mr-2" /> {isSaving ? 'Guardando...' : 'Guardar'}
<Button
onClick={() => onSave(entries)}
disabled={isSaving}
className="bg-blue-600 hover:bg-blue-700"
>
<Save className="mr-2 h-4 w-4" />{' '}
{isSaving ? 'Guardando...' : 'Guardar'}
</Button>
</div>
</div>
@@ -133,14 +215,16 @@ export function BibliographyItem({ bibliografia, onSave, isSaving }: Bibliografi
{/* BASICA */}
<section className="space-y-4">
<div className="flex items-center gap-2">
<div className="h-4 w-1 bg-blue-600 rounded-full" />
<h3 className="font-semibold text-slate-800">Bibliografía Básica</h3>
<div className="h-4 w-1 rounded-full bg-blue-600" />
<h3 className="font-semibold text-slate-800">
Bibliografía Básica
</h3>
</div>
<div className="grid gap-3">
{basicaEntries.map(entry => (
<BibliografiaCard
key={entry.id}
entry={entry}
{basicaEntries.map((entry) => (
<BibliografiaCard
key={entry.id}
entry={entry}
isEditing={editingId === entry.id}
onEdit={() => setEditingId(entry.id)}
onStopEditing={() => setEditingId(null)}
@@ -154,14 +238,16 @@ export function BibliographyItem({ bibliografia, onSave, isSaving }: Bibliografi
{/* COMPLEMENTARIA */}
<section className="space-y-4">
<div className="flex items-center gap-2">
<div className="h-4 w-1 bg-slate-400 rounded-full" />
<h3 className="font-semibold text-slate-800">Bibliografía Complementaria</h3>
<div className="h-4 w-1 rounded-full bg-slate-400" />
<h3 className="font-semibold text-slate-800">
Bibliografía Complementaria
</h3>
</div>
<div className="grid gap-3">
{complementariaEntries.map(entry => (
<BibliografiaCard
key={entry.id}
entry={entry}
{complementariaEntries.map((entry) => (
<BibliografiaCard
key={entry.id}
entry={entry}
isEditing={editingId === entry.id}
onEdit={() => setEditingId(entry.id)}
onStopEditing={() => setEditingId(null)}
@@ -177,70 +263,143 @@ export function BibliographyItem({ bibliografia, onSave, isSaving }: Bibliografi
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>¿Eliminar referencia?</AlertDialogTitle>
<AlertDialogDescription>La referencia será quitada del plan de estudios.</AlertDialogDescription>
<AlertDialogDescription>
La referencia será quitada del plan de estudios.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancelar</AlertDialogCancel>
<AlertDialogAction onClick={() => { setEntries(entries.filter(e => e.id !== deleteId)); setDeleteId(null); }} className="bg-red-600">Eliminar</AlertDialogAction>
<AlertDialogAction
onClick={() => {
setEntries(entries.filter((e) => e.id !== deleteId))
setDeleteId(null)
}}
className="bg-red-600"
>
Eliminar
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
);
)
}
// --- Subcomponentes ---
function BibliografiaCard({ entry, isEditing, onEdit, onStopEditing, onUpdateCita, onDelete }: any) {
const [localCita, setLocalCita] = useState(entry.cita);
function BibliografiaCard({
entry,
isEditing,
onEdit,
onStopEditing,
onUpdateCita,
onDelete,
}: any) {
const [localCita, setLocalCita] = useState(entry.cita)
return (
<Card className={cn("group transition-all hover:shadow-md", isEditing && "ring-2 ring-blue-500")}>
<Card
className={cn(
'group transition-all hover:shadow-md',
isEditing && 'ring-2 ring-blue-500',
)}
>
<CardContent className="p-4">
<div className="flex items-start gap-4">
<BookOpen className={cn("w-5 h-5 mt-1", entry.tipo === 'BASICA' ? "text-blue-600" : "text-slate-400")} />
<div className="flex-1 min-w-0">
<BookOpen
className={cn(
'mt-1 h-5 w-5',
entry.tipo === 'BASICA' ? 'text-blue-600' : 'text-slate-400',
)}
/>
<div className="min-w-0 flex-1">
{isEditing ? (
<div className="space-y-2">
<Textarea value={localCita} onChange={(e) => setLocalCita(e.target.value)} className="min-h-[80px]" />
<Textarea
value={localCita}
onChange={(e) => setLocalCita(e.target.value)}
className="min-h-[80px]"
/>
<div className="flex justify-end gap-2">
<Button variant="ghost" size="sm" onClick={onStopEditing}>Cancelar</Button>
<Button size="sm" className="bg-emerald-600" onClick={() => { onUpdateCita(entry.id, localCita); onStopEditing(); }}>Guardar</Button>
<Button variant="ghost" size="sm" onClick={onStopEditing}>
Cancelar
</Button>
<Button
size="sm"
className="bg-emerald-600"
onClick={() => {
onUpdateCita(entry.id, localCita)
onStopEditing()
}}
>
Guardar
</Button>
</div>
</div>
) : (
<div onClick={onEdit} className="cursor-pointer">
<p className="text-sm leading-relaxed text-slate-700">{entry.cita}</p>
<p className="text-sm leading-relaxed text-slate-700">
{entry.cita}
</p>
{entry.fuenteBiblioteca && (
<div className="flex gap-2 mt-2">
<Badge variant="secondary" className="text-[10px] bg-slate-100 text-slate-600">Biblioteca</Badge>
{entry.fuenteBiblioteca.disponible && <Badge className="text-[10px] bg-emerald-50 text-emerald-700 border-emerald-100">Disponible</Badge>}
<div className="mt-2 flex gap-2">
<Badge
variant="secondary"
className="bg-slate-100 text-[10px] text-slate-600"
>
Biblioteca
</Badge>
{entry.fuenteBiblioteca.disponible && (
<Badge className="border-emerald-100 bg-emerald-50 text-[10px] text-emerald-700">
Disponible
</Badge>
)}
</div>
)}
</div>
)}
</div>
{!isEditing && (
<div className="flex opacity-0 group-hover:opacity-100 transition-opacity">
<Button variant="ghost" size="icon" className="h-8 w-8 text-slate-400 hover:text-blue-600" onClick={onEdit}><Edit3 className="w-4 h-4" /></Button>
<Button variant="ghost" size="icon" className="h-8 w-8 text-slate-400 hover:text-red-500" onClick={onDelete}><Trash2 className="w-4 h-4" /></Button>
<div className="flex opacity-0 transition-opacity group-hover:opacity-100">
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-slate-400 hover:text-blue-600"
onClick={onEdit}
>
<Edit3 className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-slate-400 hover:text-red-500"
onClick={onDelete}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
)}
</div>
</CardContent>
</Card>
);
)
}
function AddManualDialog({ tipo, onTypeChange, onAdd }: any) {
const [cita, setCita] = useState('');
const [cita, setCita] = useState('')
return (
<div className="space-y-4 py-4">
<DialogHeader><DialogTitle>Referencia Manual</DialogTitle></DialogHeader>
<DialogHeader>
<DialogTitle>Referencia Manual</DialogTitle>
</DialogHeader>
<div className="space-y-2">
<label className="text-xs font-bold uppercase text-slate-500">Tipo</label>
<label className="text-xs font-bold text-slate-500 uppercase">
Tipo
</label>
<Select value={tipo} onValueChange={onTypeChange}>
<SelectTrigger><SelectValue /></SelectTrigger>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="BASICA">Básica</SelectItem>
<SelectItem value="COMPLEMENTARIA">Complementaria</SelectItem>
@@ -248,44 +407,78 @@ function AddManualDialog({ tipo, onTypeChange, onAdd }: any) {
</Select>
</div>
<div className="space-y-2">
<label className="text-xs font-bold uppercase text-slate-500">Cita APA</label>
<Textarea value={cita} onChange={(e) => setCita(e.target.value)} placeholder="Autor, A. (Año). Título..." className="min-h-[120px]" />
<label className="text-xs font-bold text-slate-500 uppercase">
Cita APA
</label>
<Textarea
value={cita}
onChange={(e) => setCita(e.target.value)}
placeholder="Autor, A. (Año). Título..."
className="min-h-[120px]"
/>
</div>
<Button onClick={() => onAdd(cita)} disabled={!cita.trim()} className="w-full bg-blue-600">Añadir a la lista</Button>
<Button
onClick={() => onAdd(cita)}
disabled={!cita.trim()}
className="w-full bg-blue-600"
>
Añadir a la lista
</Button>
</div>
);
)
}
function LibrarySearchDialog({ onSelect, existingIds }: any) {
const [search, setSearch] = useState('');
const [tipo, setTipo] = useState<'BASICA' | 'COMPLEMENTARIA'>('BASICA');
const filtered = mockLibraryResources.filter(r =>
!existingIds.includes(r.id) && r.titulo.toLowerCase().includes(search.toLowerCase())
);
const [search, setSearch] = useState('')
const [tipo, setTipo] = useState<'BASICA' | 'COMPLEMENTARIA'>('BASICA')
const filtered = mockLibraryResources.filter(
(r) =>
!existingIds.includes(r.id) &&
r.titulo.toLowerCase().includes(search.toLowerCase()),
)
return (
<div className="space-y-4 py-2">
<DialogHeader><DialogTitle>Catálogo de Biblioteca</DialogTitle></DialogHeader>
<DialogHeader>
<DialogTitle>Catálogo de Biblioteca</DialogTitle>
</DialogHeader>
<div className="flex gap-2">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
<Input value={search} onChange={(e) => setSearch(e.target.value)} placeholder="Buscar por título o autor..." className="pl-10" />
<Search className="absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-slate-400" />
<Input
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Buscar por título o autor..."
className="pl-10"
/>
</div>
<Select value={tipo} onValueChange={(v:any) => setTipo(v)}><SelectTrigger className="w-36"><SelectValue /></SelectTrigger>
<SelectContent><SelectItem value="BASICA">Básica</SelectItem><SelectItem value="COMPLEMENTARIA">Complem.</SelectItem></SelectContent>
<Select value={tipo} onValueChange={(v: any) => setTipo(v)}>
<SelectTrigger className="w-36">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="BASICA">Básica</SelectItem>
<SelectItem value="COMPLEMENTARIA">Complem.</SelectItem>
</SelectContent>
</Select>
</div>
<div className="max-h-[300px] overflow-y-auto pr-2 space-y-2">
{filtered.map(res => (
<div key={res.id} onClick={() => onSelect(res, tipo)} className="p-3 border rounded-lg hover:bg-slate-50 cursor-pointer flex justify-between items-center group">
<div className="max-h-[300px] space-y-2 overflow-y-auto pr-2">
{filtered.map((res) => (
<div
key={res.id}
onClick={() => onSelect(res, tipo)}
className="group flex cursor-pointer items-center justify-between rounded-lg border p-3 hover:bg-slate-50"
>
<div>
<p className="text-sm font-semibold text-slate-700">{res.titulo}</p>
<p className="text-sm font-semibold text-slate-700">
{res.titulo}
</p>
<p className="text-xs text-slate-500">{res.autor}</p>
</div>
<Plus className="w-4 h-4 text-blue-600 opacity-0 group-hover:opacity-100 transition-opacity" />
<Plus className="h-4 w-4 text-blue-600 opacity-0 transition-opacity group-hover:opacity-100" />
</div>
))}
</div>
</div>
);
}
)
}

View File

@@ -1,10 +1,23 @@
import { useState } from 'react';
import { Plus, GripVertical, ChevronDown, ChevronRight, Edit3, Trash2, Clock, Save } from 'lucide-react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Badge } from '@/components/ui/badge';
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
import { useEffect, useState } from 'react'
import {
Plus,
GripVertical,
ChevronDown,
ChevronRight,
Edit3,
Trash2,
Clock,
Save,
} from 'lucide-react'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Badge } from '@/components/ui/badge'
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from '@/components/ui/collapsible'
import {
AlertDialog,
AlertDialogAction,
@@ -14,24 +27,22 @@ import {
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog';
import { cn } from '@/lib/utils';
} from '@/components/ui/alert-dialog'
import { cn } from '@/lib/utils'
//import { toast } from 'sonner';
export interface Tema {
id: string;
nombre: string;
descripcion?: string;
horasEstimadas?: number;
id: string
nombre: string
descripcion?: string
horasEstimadas?: number
}
export interface UnidadTematica {
id: string;
nombre: string;
numero: number;
temas: Tema[];
id: string
nombre: string
numero: number
temas: Tema[]
}
const initialData: UnidadTematica[] = [
@@ -42,152 +53,297 @@ const initialData: UnidadTematica[] = [
temas: [
{ id: 't1', nombre: 'Tipos de IA y aplicaciones', horasEstimadas: 6 },
{ id: 't2', nombre: 'Ética en IA', horasEstimadas: 3 },
]
}
];
],
},
]
export function ContenidoTematico() {
const [unidades, setUnidades] = useState<UnidadTematica[]>(initialData);
const [expandedUnits, setExpandedUnits] = useState<Set<string>>(new Set(['u1']));
const [deleteDialog, setDeleteDialog] = useState<{ type: 'unidad' | 'tema'; id: string; parentId?: string } | null>(null);
const [editingUnit, setEditingUnit] = useState<string | null>(null);
const [editingTema, setEditingTema] = useState<{ unitId: string; temaId: string } | null>(null);
const [isSaving, setIsSaving] = useState(false);
// Estructura que viene de tu JSON/API
interface ContenidoApi {
unidad: number
titulo: string
temas: string[] | any[] // Acepta strings o objetos
[key: string]: any // Esta línea permite que haya más claves desconocidas
}
// Props del componente
interface ContenidoTematicoProps {
data: {
contenido_tematico: ContenidoApi[]
}
isLoading: boolean
}
export function ContenidoTematico({ data, isLoading }: ContenidoTematicoProps) {
const [unidades, setUnidades] = useState<UnidadTematica[]>([])
const [expandedUnits, setExpandedUnits] = useState<Set<string>>(
new Set(['u1']),
)
const [deleteDialog, setDeleteDialog] = useState<{
type: 'unidad' | 'tema'
id: string
parentId?: string
} | null>(null)
const [editingUnit, setEditingUnit] = useState<string | null>(null)
const [editingTema, setEditingTema] = useState<{
unitId: string
temaId: string
} | null>(null)
const [isSaving, setIsSaving] = useState(false)
useEffect(() => {
if (data?.contenido_tematico) {
const transformed = data.contenido_tematico.map(
(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
if (transformed.length > 0) {
setExpandedUnits(new Set([transformed[0].id]))
}
}
}, [data])
if (isLoading)
return <div className="p-10 text-center">Cargando contenido...</div>
// 3. Cálculo de horas (ahora dinámico basado en los nuevos datos)
const totalHoras = unidades.reduce(
(acc, u) =>
acc + u.temas.reduce((sum, t) => sum + (t.horasEstimadas || 0), 0),
0,
)
// --- Lógica de Unidades ---
const toggleUnit = (id: string) => {
const newExpanded = new Set(expandedUnits);
newExpanded.has(id) ? newExpanded.delete(id) : newExpanded.add(id);
setExpandedUnits(newExpanded);
};
const newExpanded = new Set(expandedUnits)
newExpanded.has(id) ? newExpanded.delete(id) : newExpanded.add(id)
setExpandedUnits(newExpanded)
}
const addUnidad = () => {
const newId = `u-${Date.now()}`;
const newId = `u-${Date.now()}`
const newUnidad: UnidadTematica = {
id: newId,
nombre: 'Nueva Unidad',
numero: unidades.length + 1,
temas: [],
};
setUnidades([...unidades, newUnidad]);
setExpandedUnits(new Set([...expandedUnits, newId]));
setEditingUnit(newId);
};
}
setUnidades([...unidades, newUnidad])
setExpandedUnits(new Set([...expandedUnits, newId]))
setEditingUnit(newId)
}
const updateUnidadNombre = (id: string, nombre: string) => {
setUnidades(unidades.map(u => u.id === id ? { ...u, nombre } : u));
};
setUnidades(unidades.map((u) => (u.id === id ? { ...u, nombre } : u)))
}
// --- Lógica de Temas ---
const addTema = (unidadId: string) => {
setUnidades(unidades.map(u => {
if (u.id === unidadId) {
const newTemaId = `t-${Date.now()}`;
const newTema: Tema = { id: newTemaId, nombre: 'Nuevo tema', horasEstimadas: 2 };
setEditingTema({ unitId: unidadId, temaId: newTemaId });
return { ...u, temas: [...u.temas, newTema] };
}
return u;
}));
};
setUnidades(
unidades.map((u) => {
if (u.id === unidadId) {
const newTemaId = `t-${Date.now()}`
const newTema: Tema = {
id: newTemaId,
nombre: 'Nuevo tema',
horasEstimadas: 2,
}
setEditingTema({ unitId: unidadId, temaId: newTemaId })
return { ...u, temas: [...u.temas, newTema] }
}
return u
}),
)
}
const updateTema = (unidadId: string, temaId: string, updates: Partial<Tema>) => {
setUnidades(unidades.map(u => {
if (u.id === unidadId) {
return { ...u, temas: u.temas.map(t => t.id === temaId ? { ...t, ...updates } : t) };
}
return u;
}));
};
const updateTema = (
unidadId: string,
temaId: string,
updates: Partial<Tema>,
) => {
setUnidades(
unidades.map((u) => {
if (u.id === unidadId) {
return {
...u,
temas: u.temas.map((t) =>
t.id === temaId ? { ...t, ...updates } : t,
),
}
}
return u
}),
)
}
const handleDelete = () => {
if (!deleteDialog) return;
if (!deleteDialog) return
if (deleteDialog.type === 'unidad') {
setUnidades(unidades.filter(u => u.id !== deleteDialog.id).map((u, i) => ({ ...u, numero: i + 1 })));
setUnidades(
unidades
.filter((u) => u.id !== deleteDialog.id)
.map((u, i) => ({ ...u, numero: i + 1 })),
)
} else if (deleteDialog.parentId) {
setUnidades(unidades.map(u => u.id === deleteDialog.parentId ? { ...u, temas: u.temas.filter(t => t.id !== deleteDialog.id) } : u));
setUnidades(
unidades.map((u) =>
u.id === deleteDialog.parentId
? { ...u, temas: u.temas.filter((t) => t.id !== deleteDialog.id) }
: u,
),
)
}
setDeleteDialog(null);
setDeleteDialog(null)
//toast.success("Eliminado correctamente");
};
const totalHoras = unidades.reduce((acc, u) => acc + u.temas.reduce((sum, t) => sum + (t.horasEstimadas || 0), 0), 0);
}
return (
<div className="max-w-5xl mx-auto py-10 space-y-6 animate-in fade-in duration-500">
<div className="animate-in fade-in mx-auto max-w-5xl space-y-6 py-10 duration-500">
<div className="flex items-center justify-between border-b pb-4">
<div>
<h2 className="text-2xl font-bold tracking-tight text-slate-900">Contenido Temático</h2>
<p className="text-sm text-slate-500 mt-1">
<h2 className="text-2xl font-bold tracking-tight text-slate-900">
Contenido Temático
</h2>
<p className="mt-1 text-sm text-slate-500">
{unidades.length} unidades {totalHoras} horas estimadas totales
</p>
</div>
<div className="flex items-center gap-2">
<Button variant="outline" onClick={addUnidad} className="gap-2">
<Plus className="w-4 h-4" /> Nueva unidad
<Plus className="h-4 w-4" /> Nueva unidad
</Button>
<Button onClick={() => { setIsSaving(true); setTimeout(() => { setIsSaving(false); /*toast.success("Guardado")*/; }, 1000); }} disabled={isSaving} className="bg-blue-600 hover:bg-blue-700">
<Save className="w-4 h-4 mr-2" /> {isSaving ? 'Guardando...' : 'Guardar'}
<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 className="space-y-4">
{unidades.map((unidad) => (
<Card key={unidad.id} className="overflow-hidden border-slate-200 shadow-sm">
<Collapsible open={expandedUnits.has(unidad.id)} onOpenChange={() => toggleUnit(unidad.id)}>
<CardHeader className="bg-slate-50/50 py-3 border-b border-slate-100">
<Card
key={unidad.id}
className="overflow-hidden border-slate-200 shadow-sm"
>
<Collapsible
open={expandedUnits.has(unidad.id)}
onOpenChange={() => toggleUnit(unidad.id)}
>
<CardHeader className="border-b border-slate-100 bg-slate-50/50 py-3">
<div className="flex items-center gap-3">
<GripVertical className="w-4 h-4 text-slate-300 cursor-grab" />
<GripVertical className="h-4 w-4 cursor-grab text-slate-300" />
<CollapsibleTrigger asChild>
<Button variant="ghost" size="sm" className="p-0 h-auto">
{expandedUnits.has(unidad.id) ? <ChevronDown className="w-4 h-4" /> : <ChevronRight className="w-4 h-4" />}
<Button variant="ghost" size="sm" className="h-auto p-0">
{expandedUnits.has(unidad.id) ? (
<ChevronDown className="h-4 w-4" />
) : (
<ChevronRight className="h-4 w-4" />
)}
</Button>
</CollapsibleTrigger>
<Badge className="bg-blue-600 font-mono">Unidad {unidad.numero}</Badge>
<Badge className="bg-blue-600 font-mono">
Unidad {unidad.numero}
</Badge>
{editingUnit === unidad.id ? (
<Input
value={unidad.nombre}
onChange={(e) => updateUnidadNombre(unidad.id, e.target.value)}
<Input
value={unidad.nombre}
onChange={(e) =>
updateUnidadNombre(unidad.id, e.target.value)
}
onBlur={() => setEditingUnit(null)}
onKeyDown={(e) => e.key === 'Enter' && setEditingUnit(null)}
className="max-w-md h-8 bg-white"
autoFocus
onKeyDown={(e) =>
e.key === 'Enter' && setEditingUnit(null)
}
className="h-8 max-w-md bg-white"
autoFocus
/>
) : (
<CardTitle className="text-base font-semibold cursor-pointer hover:text-blue-600 transition-colors" onClick={() => setEditingUnit(unidad.id)}>
<CardTitle
className="cursor-pointer text-base font-semibold transition-colors hover:text-blue-600"
onClick={() => setEditingUnit(unidad.id)}
>
{unidad.nombre}
</CardTitle>
)}
<div className="ml-auto flex items-center gap-3">
<span className="text-xs font-medium text-slate-400 flex items-center gap-1">
<Clock className="w-3 h-3" /> {unidad.temas.reduce((sum, t) => sum + (t.horasEstimadas || 0), 0)}h
<span className="flex items-center gap-1 text-xs font-medium text-slate-400">
<Clock className="h-3 w-3" />{' '}
{unidad.temas.reduce(
(sum, t) => sum + (t.horasEstimadas || 0),
0,
)}
h
</span>
<Button variant="ghost" size="icon" className="h-8 w-8 text-slate-400 hover:text-red-500" onClick={() => setDeleteDialog({ type: 'unidad', id: unidad.id })}>
<Trash2 className="w-4 h-4" />
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-slate-400 hover:text-red-500"
onClick={() =>
setDeleteDialog({ type: 'unidad', id: unidad.id })
}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</div>
</CardHeader>
<CollapsibleContent>
<CardContent className="pt-4 bg-white">
<div className="space-y-1 ml-10 border-l-2 border-slate-50 pl-4">
<CardContent className="bg-white pt-4">
<div className="ml-10 space-y-1 border-l-2 border-slate-50 pl-4">
{unidad.temas.map((tema, idx) => (
<TemaRow
key={tema.id}
tema={tema}
index={idx + 1}
isEditing={editingTema?.unitId === unidad.id && editingTema?.temaId === tema.id}
onEdit={() => setEditingTema({ unitId: unidad.id, temaId: tema.id })}
<TemaRow
key={tema.id}
tema={tema}
index={idx + 1}
isEditing={
editingTema?.unitId === unidad.id &&
editingTema?.temaId === tema.id
}
onEdit={() =>
setEditingTema({ unitId: unidad.id, temaId: tema.id })
}
onStopEditing={() => setEditingTema(null)}
onUpdate={(updates) => updateTema(unidad.id, tema.id, updates)}
onDelete={() => setDeleteDialog({ type: 'tema', id: tema.id, parentId: unidad.id })}
onUpdate={(updates) =>
updateTema(unidad.id, tema.id, updates)
}
onDelete={() =>
setDeleteDialog({
type: 'tema',
id: tema.id,
parentId: unidad.id,
})
}
/>
))}
<Button variant="ghost" size="sm" className="text-blue-600 hover:text-blue-700 hover:bg-blue-50 w-full justify-start mt-2" onClick={() => addTema(unidad.id)}>
<Plus className="w-3 h-3 mr-2" /> Añadir subtema
<Button
variant="ghost"
size="sm"
className="mt-2 w-full justify-start text-blue-600 hover:bg-blue-50 hover:text-blue-700"
onClick={() => addTema(unidad.id)}
>
<Plus className="mr-2 h-3 w-3" /> Añadir subtema
</Button>
</div>
</CardContent>
@@ -197,81 +353,137 @@ export function ContenidoTematico() {
))}
</div>
<DeleteConfirmDialog dialog={deleteDialog} setDialog={setDeleteDialog} onConfirm={handleDelete} />
<DeleteConfirmDialog
dialog={deleteDialog}
setDialog={setDeleteDialog}
onConfirm={handleDelete}
/>
</div>
);
)
}
// --- Componentes Auxiliares ---
interface TemaRowProps {
tema: Tema;
index: number;
isEditing: boolean;
onEdit: () => void;
onStopEditing: () => void;
onUpdate: (updates: Partial<Tema>) => void;
onDelete: () => void;
tema: Tema
index: number
isEditing: boolean
onEdit: () => void
onStopEditing: () => void
onUpdate: (updates: Partial<Tema>) => void
onDelete: () => void
}
function TemaRow({ tema, index, isEditing, onEdit, onStopEditing, onUpdate, onDelete }: TemaRowProps) {
function TemaRow({
tema,
index,
isEditing,
onEdit,
onStopEditing,
onUpdate,
onDelete,
}: TemaRowProps) {
return (
<div className={cn("flex items-center gap-3 p-2 rounded-md group transition-all", isEditing ? "bg-blue-50 ring-1 ring-blue-100" : "hover:bg-slate-50")}>
<span className="text-xs font-mono text-slate-400 w-4">{index}.</span>
<div
className={cn(
'group flex items-center gap-3 rounded-md p-2 transition-all',
isEditing ? 'bg-blue-50 ring-1 ring-blue-100' : 'hover:bg-slate-50',
)}
>
<span className="w-4 font-mono text-xs text-slate-400">{index}.</span>
{isEditing ? (
<div className="flex-1 flex items-center gap-2 animate-in slide-in-from-left-2">
<Input value={tema.nombre} onChange={(e) => onUpdate({ nombre: e.target.value })} className="h-8 flex-1 bg-white" placeholder="Nombre" autoFocus />
<Input type="number" value={tema.horasEstimadas} onChange={(e) => onUpdate({ horasEstimadas: parseInt(e.target.value) || 0 })} className="h-8 w-16 bg-white" />
<Button size="sm" className="bg-emerald-600 h-8" onClick={onStopEditing}>Listo</Button>
<div className="animate-in slide-in-from-left-2 flex flex-1 items-center gap-2">
<Input
value={tema.nombre}
onChange={(e) => onUpdate({ nombre: e.target.value })}
className="h-8 flex-1 bg-white"
placeholder="Nombre"
autoFocus
/>
<Input
type="number"
value={tema.horasEstimadas}
onChange={(e) =>
onUpdate({ horasEstimadas: parseInt(e.target.value) || 0 })
}
className="h-8 w-16 bg-white"
/>
<Button
size="sm"
className="h-8 bg-emerald-600"
onClick={onStopEditing}
>
Listo
</Button>
</div>
) : (
<>
<div className="flex-1 cursor-pointer" onClick={onEdit}>
<p className="text-sm font-medium text-slate-700">{tema.nombre}</p>
</div>
<Badge variant="secondary" className="text-[10px] opacity-60">{tema.horasEstimadas}h</Badge>
<div className="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
<Button variant="ghost" size="icon" className="h-7 w-7 text-slate-400 hover:text-blue-600" onClick={onEdit}><Edit3 className="w-3 h-3" /></Button>
<Button variant="ghost" size="icon" className="h-7 w-7 text-slate-400 hover:text-red-500" onClick={onDelete}><Trash2 className="w-3 h-3" /></Button>
<Badge variant="secondary" className="text-[10px] opacity-60">
{tema.horasEstimadas}h
</Badge>
<div className="flex items-center gap-1 opacity-0 transition-opacity group-hover:opacity-100">
<Button
variant="ghost"
size="icon"
className="h-7 w-7 text-slate-400 hover:text-blue-600"
onClick={onEdit}
>
<Edit3 className="h-3 w-3" />
</Button>
<Button
variant="ghost"
size="icon"
className="h-7 w-7 text-slate-400 hover:text-red-500"
onClick={onDelete}
>
<Trash2 className="h-3 w-3" />
</Button>
</div>
</>
)}
</div>
);
)
}
interface DeleteDialogState {
type: 'unidad' | 'tema';
id: string;
parentId?: string;
type: 'unidad' | 'tema'
id: string
parentId?: string
}
interface DeleteConfirmDialogProps {
dialog: DeleteDialogState | null;
setDialog: (value: DeleteDialogState | null) => void;
onConfirm: () => void;
dialog: DeleteDialogState | null
setDialog: (value: DeleteDialogState | null) => void
onConfirm: () => void
}
function DeleteConfirmDialog({
dialog,
setDialog,
onConfirm,
}: DeleteConfirmDialogProps) {
return (
<AlertDialog open={!!dialog} onOpenChange={() => setDialog(null)}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>¿Confirmar eliminación?</AlertDialogTitle>
<AlertDialogDescription>
Estás a punto de borrar un {dialog?.type}. Esta acción no se puede deshacer.
Estás a punto de borrar un {dialog?.type}. Esta acción no se puede
deshacer.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancelar</AlertDialogCancel>
<AlertDialogAction onClick={onConfirm} className="bg-red-600 hover:bg-red-700 text-white">Eliminar</AlertDialogAction>
<AlertDialogAction
onClick={onConfirm}
className="bg-red-600 text-white hover:bg-red-700"
>
Eliminar
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
}
)
}

View File

@@ -1,8 +1,16 @@
import { useState } from 'react';
import { FileText, Download, RefreshCw, Calendar, FileCheck, AlertTriangle, Loader2 } from 'lucide-react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { useState } from 'react'
import {
FileText,
Download,
RefreshCw,
Calendar,
FileCheck,
AlertTriangle,
Loader2,
} from 'lucide-react'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import {
AlertDialog,
AlertDialogAction,
@@ -13,63 +21,88 @@ import {
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from '@/components/ui/alert-dialog';
import type { DocumentoMateria, Materia, MateriaStructure } from '@/types/materia';
import { cn } from '@/lib/utils';
} from '@/components/ui/alert-dialog'
import type {
DocumentoMateria,
Materia,
MateriaStructure,
} from '@/types/materia'
import { cn } from '@/lib/utils'
import { useSubjectBibliografia } from '@/data/hooks/useSubjects'
//import { toast } from 'sonner';
//import { format } from 'date-fns';
//import { es } from 'date-fns/locale';
interface DocumentoSEPTabProps {
documento: DocumentoMateria | null;
materia: Materia;
estructura: MateriaStructure;
datosGenerales: Record<string, any>;
onRegenerate: () => void;
isRegenerating: boolean;
documento: DocumentoMateria | null
materia: Materia
estructura: MateriaStructure
datosGenerales: Record<string, any>
onRegenerate: () => void
isRegenerating: boolean
}
export function DocumentoSEPTab({ documento, materia, estructura, datosGenerales, onRegenerate, isRegenerating }: DocumentoSEPTabProps) {
const [showConfirmDialog, setShowConfirmDialog] = useState(false);
export function DocumentoSEPTab({
documento,
materia,
estructura,
datosGenerales,
onRegenerate,
isRegenerating,
}: DocumentoSEPTabProps) {
const [showConfirmDialog, setShowConfirmDialog] = useState(false)
// Check completeness
const camposObligatorios = estructura.campos.filter(c => c.obligatorio);
const camposCompletos = camposObligatorios.filter(c => datosGenerales[c.id]?.trim());
const completeness = Math.round((camposCompletos.length / camposObligatorios.length) * 100);
const isComplete = completeness === 100;
const camposObligatorios = estructura.campos.filter((c) => c.obligatorio)
const camposCompletos = camposObligatorios.filter((c) =>
datosGenerales[c.id]?.trim(),
)
const completeness = Math.round(
(camposCompletos.length / camposObligatorios.length) * 100,
)
const isComplete = completeness === 100
const handleRegenerate = () => {
setShowConfirmDialog(false);
onRegenerate();
setShowConfirmDialog(false)
onRegenerate()
//toast.success('Regenerando documento...');
};
}
return (
<div className="space-y-6 animate-fade-in">
<div className="animate-fade-in space-y-6">
<div className="flex items-center justify-between">
<div>
<h2 className="font-display text-2xl font-semibold text-foreground flex items-center gap-2">
<FileCheck className="w-6 h-6 text-accent" />
<h2 className="font-display text-foreground flex items-center gap-2 text-2xl font-semibold">
<FileCheck className="text-accent h-6 w-6" />
Documento SEP
</h2>
<p className="text-sm text-muted-foreground mt-1">
<p className="text-muted-foreground mt-1 text-sm">
Previsualización del documento oficial para la SEP
</p>
</div>
<div className="flex items-center gap-2">
{documento?.estado === 'listo' && (
<Button variant="outline" onClick={() => console.log("descargando") /*toast.info('Descarga iniciada')*/}>
<Download className="w-4 h-4 mr-2" />
<Button
variant="outline"
onClick={
() =>
console.log('descargando') /*toast.info('Descarga iniciada')*/
}
>
<Download className="mr-2 h-4 w-4" />
Descargar
</Button>
)}
<AlertDialog open={showConfirmDialog} onOpenChange={setShowConfirmDialog}>
<AlertDialog
open={showConfirmDialog}
onOpenChange={setShowConfirmDialog}
>
<AlertDialogTrigger asChild>
<Button disabled={isRegenerating || !isComplete}>
{isRegenerating ? (
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
) : (
<RefreshCw className="w-4 h-4 mr-2" />
<RefreshCw className="mr-2 h-4 w-4" />
)}
{isRegenerating ? 'Generando...' : 'Regenerar documento'}
</Button>
@@ -78,8 +111,9 @@ export function DocumentoSEPTab({ documento, materia, estructura, datosGenerales
<AlertDialogHeader>
<AlertDialogTitle>¿Regenerar documento SEP?</AlertDialogTitle>
<AlertDialogDescription>
Se creará una nueva versión del documento con los datos actuales de la materia.
La versión anterior quedará en el historial.
Se creará una nueva versión del documento con los datos
actuales de la materia. La versión anterior quedará en el
historial.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
@@ -93,91 +127,108 @@ export function DocumentoSEPTab({ documento, materia, estructura, datosGenerales
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
{/* Document preview */}
<div className="lg:col-span-2">
<Card className="card-elevated h-[700px] overflow-hidden">
{documento?.estado === 'listo' ? (
<div className="h-full bg-muted/30 flex flex-col">
<div className="bg-muted/30 flex h-full flex-col">
{/* Simulated document header */}
<div className="bg-card border-b p-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<FileText className="w-5 h-5 text-primary" />
<span className="font-medium text-foreground">
<FileText className="text-primary h-5 w-5" />
<span className="text-foreground font-medium">
Programa de Estudios - {materia.clave}
</span>
</div>
<Badge variant="outline">Versión {documento.version}</Badge>
</div>
</div>
{/* Document content simulation */}
<div className="flex-1 overflow-y-auto p-8">
<div className="max-w-2xl mx-auto bg-card rounded-lg shadow-lg p-8 space-y-6">
<div className="bg-card mx-auto max-w-2xl space-y-6 rounded-lg p-8 shadow-lg">
{/* Header */}
<div className="text-center border-b pb-6">
<p className="text-xs text-muted-foreground uppercase tracking-wide mb-2">
<div className="border-b pb-6 text-center">
<p className="text-muted-foreground mb-2 text-xs tracking-wide uppercase">
Secretaría de Educación Pública
</p>
<h1 className="font-display text-2xl font-bold text-primary mb-1">
<h1 className="font-display text-primary mb-1 text-2xl font-bold">
{materia.nombre}
</h1>
<p className="text-sm text-muted-foreground">
Clave: {materia.clave} | Créditos: {materia.creditos || 'N/A'}
<p className="text-muted-foreground text-sm">
Clave: {materia.clave} | Créditos:{' '}
{materia.creditos || 'N/A'}
</p>
</div>
{/* Datos de la institución */}
<div className="space-y-1 text-sm">
<p><strong>Carrera:</strong> {materia.carrera}</p>
<p><strong>Facultad:</strong> {materia.facultad}</p>
<p><strong>Plan de estudios:</strong> {materia.planNombre}</p>
{materia.ciclo && <p><strong>Ciclo:</strong> {materia.ciclo}</p>}
<p>
<strong>Carrera:</strong> {materia.carrera}
</p>
<p>
<strong>Facultad:</strong> {materia.facultad}
</p>
<p>
<strong>Plan de estudios:</strong> {materia.planNombre}
</p>
{materia.ciclo && (
<p>
<strong>Ciclo:</strong> {materia.ciclo}
</p>
)}
</div>
{/* Campos del documento */}
{estructura.campos.map((campo) => {
const valor = datosGenerales[campo.id];
if (!valor) return null;
const valor = datosGenerales[campo.id]
if (!valor) return null
return (
<div key={campo.id} className="space-y-2">
<h3 className="font-semibold text-foreground border-b pb-1">
<h3 className="text-foreground border-b pb-1 font-semibold">
{campo.nombre}
</h3>
<p className="text-sm text-foreground whitespace-pre-wrap leading-relaxed">
<p className="text-foreground text-sm leading-relaxed whitespace-pre-wrap">
{valor}
</p>
</div>
);
)
})}
{/* Footer */}
<div className="border-t pt-6 mt-8 text-center text-xs text-muted-foreground">
<p>Documento generado el {/*format(documento.fechaGeneracion, "d 'de' MMMM 'de' yyyy", { locale: es })*/}</p>
<div className="text-muted-foreground mt-8 border-t pt-6 text-center text-xs">
<p>
Documento generado el{' '}
{/*format(documento.fechaGeneracion, "d 'de' MMMM 'de' yyyy", { locale: es })*/}
</p>
<p className="mt-1">Universidad La Salle</p>
</div>
</div>
</div>
</div>
) : documento?.estado === 'generando' ? (
<div className="h-full flex items-center justify-center">
<div className="flex h-full items-center justify-center">
<div className="text-center">
<Loader2 className="w-12 h-12 mx-auto text-accent animate-spin mb-4" />
<p className="text-muted-foreground">Generando documento...</p>
<Loader2 className="text-accent mx-auto mb-4 h-12 w-12 animate-spin" />
<p className="text-muted-foreground">
Generando documento...
</p>
</div>
</div>
) : (
<div className="h-full flex items-center justify-center">
<div className="text-center max-w-sm">
<FileText className="w-12 h-12 mx-auto text-muted-foreground/50 mb-4" />
<div className="flex h-full items-center justify-center">
<div className="max-w-sm text-center">
<FileText className="text-muted-foreground/50 mx-auto mb-4 h-12 w-12" />
<p className="text-muted-foreground mb-4">
No hay documento generado aún
</p>
{!isComplete && (
<div className="p-4 bg-warning/10 rounded-lg text-sm text-warning-foreground">
<AlertTriangle className="w-4 h-4 inline mr-2" />
Completa todos los campos obligatorios para generar el documento
<div className="bg-warning/10 text-warning-foreground rounded-lg p-4 text-sm">
<AlertTriangle className="mr-2 inline h-4 w-4" />
Completa todos los campos obligatorios para generar el
documento
</div>
)}
</div>
@@ -191,28 +242,41 @@ export function DocumentoSEPTab({ documento, materia, estructura, datosGenerales
{/* Status */}
<Card className="card-elevated">
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium">Estado del documento</CardTitle>
<CardTitle className="text-sm font-medium">
Estado del documento
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{documento && (
<>
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">Versión</span>
<span className="text-muted-foreground text-sm">
Versión
</span>
<Badge variant="outline">{documento.version}</Badge>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">Generado</span>
<span className="text-muted-foreground text-sm">
Generado
</span>
<span className="text-sm">
{/*format(documento.fechaGeneracion, "d MMM yyyy, HH:mm", { locale: es })*/}
</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">Estado</span>
<Badge className={cn(
documento.estado === 'listo' && "bg-success text-success-foreground",
documento.estado === 'generando' && "bg-info text-info-foreground",
documento.estado === 'error' && "bg-destructive text-destructive-foreground"
)}>
<span className="text-muted-foreground text-sm">
Estado
</span>
<Badge
className={cn(
documento.estado === 'listo' &&
'bg-success text-success-foreground',
documento.estado === 'generando' &&
'bg-info text-info-foreground',
documento.estado === 'error' &&
'bg-destructive text-destructive-foreground',
)}
>
{documento.estado === 'listo' && 'Listo'}
{documento.estado === 'generando' && 'Generando'}
{documento.estado === 'error' && 'Error'}
@@ -226,44 +290,60 @@ export function DocumentoSEPTab({ documento, materia, estructura, datosGenerales
{/* Completeness */}
<Card className="card-elevated">
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium">Completitud de datos</CardTitle>
<CardTitle className="text-sm font-medium">
Completitud de datos
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground">Campos obligatorios</span>
<span className="font-medium">{camposCompletos.length}/{camposObligatorios.length}</span>
<span className="text-muted-foreground">
Campos obligatorios
</span>
<span className="font-medium">
{camposCompletos.length}/{camposObligatorios.length}
</span>
</div>
<div className="h-2 bg-muted rounded-full overflow-hidden">
<div
<div className="bg-muted h-2 overflow-hidden rounded-full">
<div
className={cn(
"h-full transition-all duration-500",
completeness === 100 ? "bg-success" : "bg-accent"
'h-full transition-all duration-500',
completeness === 100 ? 'bg-success' : 'bg-accent',
)}
style={{ width: `${completeness}%` }}
/>
</div>
<p className={cn(
"text-xs",
completeness === 100 ? "text-success" : "text-muted-foreground"
)}>
{completeness === 100
<p
className={cn(
'text-xs',
completeness === 100
? 'text-success'
: 'text-muted-foreground',
)}
>
{completeness === 100
? 'Todos los campos obligatorios están completos'
: `Faltan ${camposObligatorios.length - camposCompletos.length} campos por completar`
}
: `Faltan ${camposObligatorios.length - camposCompletos.length} campos por completar`}
</p>
</div>
{/* Missing fields */}
{!isComplete && (
<div className="space-y-2">
<p className="text-xs font-medium text-muted-foreground">Campos faltantes:</p>
{camposObligatorios.filter(c => !datosGenerales[c.id]?.trim()).map((campo) => (
<div key={campo.id} className="flex items-center gap-2 text-sm">
<AlertTriangle className="w-3 h-3 text-warning" />
<span className="text-foreground">{campo.nombre}</span>
</div>
))}
<p className="text-muted-foreground text-xs font-medium">
Campos faltantes:
</p>
{camposObligatorios
.filter((c) => !datosGenerales[c.id]?.trim())
.map((campo) => (
<div
key={campo.id}
className="flex items-center gap-2 text-sm"
>
<AlertTriangle className="text-warning h-3 w-3" />
<span className="text-foreground">{campo.nombre}</span>
</div>
))}
</div>
)}
</CardContent>
@@ -272,36 +352,62 @@ export function DocumentoSEPTab({ documento, materia, estructura, datosGenerales
{/* Requirements */}
<Card className="card-elevated">
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium">Requisitos SEP</CardTitle>
<CardTitle className="text-sm font-medium">
Requisitos SEP
</CardTitle>
</CardHeader>
<CardContent>
<ul className="space-y-2 text-sm">
<li className="flex items-start gap-2">
<div className={cn(
"w-4 h-4 rounded-full flex items-center justify-center mt-0.5",
datosGenerales['objetivo_general'] ? "bg-success/20" : "bg-muted"
)}>
{datosGenerales['objetivo_general'] && <Check className="w-3 h-3 text-success" />}
<div
className={cn(
'mt-0.5 flex h-4 w-4 items-center justify-center rounded-full',
datosGenerales['objetivo_general']
? 'bg-success/20'
: 'bg-muted',
)}
>
{datosGenerales['objetivo_general'] && (
<Check className="text-success h-3 w-3" />
)}
</div>
<span className="text-muted-foreground">Objetivo general definido</span>
<span className="text-muted-foreground">
Objetivo general definido
</span>
</li>
<li className="flex items-start gap-2">
<div className={cn(
"w-4 h-4 rounded-full flex items-center justify-center mt-0.5",
datosGenerales['competencias'] ? "bg-success/20" : "bg-muted"
)}>
{datosGenerales['competencias'] && <Check className="w-3 h-3 text-success" />}
<div
className={cn(
'mt-0.5 flex h-4 w-4 items-center justify-center rounded-full',
datosGenerales['competencias']
? 'bg-success/20'
: 'bg-muted',
)}
>
{datosGenerales['competencias'] && (
<Check className="text-success h-3 w-3" />
)}
</div>
<span className="text-muted-foreground">Competencias especificadas</span>
<span className="text-muted-foreground">
Competencias especificadas
</span>
</li>
<li className="flex items-start gap-2">
<div className={cn(
"w-4 h-4 rounded-full flex items-center justify-center mt-0.5",
datosGenerales['evaluacion'] ? "bg-success/20" : "bg-muted"
)}>
{datosGenerales['evaluacion'] && <Check className="w-3 h-3 text-success" />}
<div
className={cn(
'mt-0.5 flex h-4 w-4 items-center justify-center rounded-full',
datosGenerales['evaluacion']
? 'bg-success/20'
: 'bg-muted',
)}
>
{datosGenerales['evaluacion'] && (
<Check className="text-success h-3 w-3" />
)}
</div>
<span className="text-muted-foreground">Criterios de evaluación</span>
<span className="text-muted-foreground">
Criterios de evaluación
</span>
</li>
</ul>
</CardContent>
@@ -309,13 +415,19 @@ export function DocumentoSEPTab({ documento, materia, estructura, datosGenerales
</div>
</div>
</div>
);
)
}
function Check({ className }: { className?: string }) {
return (
<svg className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3">
<svg
className={className}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="3"
>
<polyline points="20 6 9 17 4 12" />
</svg>
);
)
}

View File

@@ -1,196 +1,230 @@
import { useState } from 'react';
import { History, FileText, List, BookMarked, Sparkles, FileCheck, User, Filter, Calendar } from 'lucide-react';
import { Card, CardContent } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { useState, useMemo } from 'react'
import {
History,
FileText,
List,
BookMarked,
Sparkles,
FileCheck,
User,
Filter,
Calendar,
Loader2,
} from 'lucide-react'
import { Card, CardContent } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuCheckboxItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import type { CambioMateria } from '@/types/materia';
import { cn } from '@/lib/utils';
import { format, formatDistanceToNow } from 'date-fns';
import { es } from 'date-fns/locale';
} from '@/components/ui/dropdown-menu'
import { cn } from '@/lib/utils'
import { format, formatDistanceToNow, parseISO } from 'date-fns'
import { es } from 'date-fns/locale'
import { useSubjectHistorial } from '@/data/hooks/useSubjects'
interface HistorialTabProps {
historial: CambioMateria[];
// Mapeo de tipos de la API a los tipos del componente
const TIPO_MAP: Record<string, string> = {
ACTUALIZACION_CAMPO: 'contenido', // O 'datos' según el campo
CREACION: 'datos',
}
const tipoConfig: Record<string, { label: string; icon: React.ComponentType<{ className?: string }>; color: string }> = {
datos: { label: 'Datos generales', icon: FileText, color: 'text-info' },
contenido: { label: 'Contenido temático', icon: List, color: 'text-accent' },
bibliografia: { label: 'Bibliografía', icon: BookMarked, color: 'text-success' },
ia: { label: 'IA', icon: Sparkles, color: 'text-amber-500' },
documento: { label: 'Documento SEP', icon: FileCheck, color: 'text-primary' },
};
const tipoConfig: Record<string, { label: string; icon: any; color: string }> =
{
datos: { label: 'Datos generales', icon: FileText, color: 'text-info' },
contenido: {
label: 'Contenido temático',
icon: List,
color: 'text-accent',
},
bibliografia: {
label: 'Bibliografía',
icon: BookMarked,
color: 'text-success',
},
ia: { label: 'IA', icon: Sparkles, color: 'text-amber-500' },
documento: {
label: 'Documento SEP',
icon: FileCheck,
color: 'text-primary',
},
}
export function HistorialTab({ historial }: HistorialTabProps) {
const [filtros, setFiltros] = useState<Set<string>>(new Set(['datos', 'contenido', 'bibliografia', 'ia', 'documento']));
export function HistorialTab() {
// 1. Obtenemos los datos directamente dentro del componente
const { data: rawData, isLoading } = useSubjectHistorial(
'9d4dda6a-488f-428a-8a07-38081592a641',
)
const [filtros, setFiltros] = useState<Set<string>>(
new Set(['datos', 'contenido', 'bibliografia', 'ia', 'documento']),
)
// 2. Transformamos los datos de la API al formato que usa el componente
const historialTransformado = useMemo(() => {
if (!rawData) return []
return rawData.map((item: any) => ({
id: item.id,
// Intentamos determinar el tipo basándonos en el campo o el tipo de la API
tipo: item.campo === 'contenido_tematico' ? 'contenido' : 'datos',
descripcion: `Se actualizó el campo ${item.campo.replace('_', ' ')}`,
fecha: parseISO(item.cambiado_en),
usuario: item.fuente === 'HUMANO' ? 'Usuario Staff' : 'Sistema IA',
detalles: {
campo: item.campo,
valor_nuevo: item.valor_nuevo,
},
}))
}, [rawData])
const toggleFiltro = (tipo: string) => {
const newFiltros = new Set(filtros);
if (newFiltros.has(tipo)) {
newFiltros.delete(tipo);
} else {
newFiltros.add(tipo);
}
setFiltros(newFiltros);
};
const newFiltros = new Set(filtros)
if (newFiltros.has(tipo)) newFiltros.delete(tipo)
else newFiltros.add(tipo)
setFiltros(newFiltros)
}
const filteredHistorial = historial.filter(cambio => filtros.has(cambio.tipo));
// 3. Aplicamos filtros y agrupamiento sobre los datos transformados
const filteredHistorial = historialTransformado.filter((cambio) =>
filtros.has(cambio.tipo),
)
// Group by date
const groupedHistorial = filteredHistorial.reduce((groups, cambio) => {
const dateKey = format(cambio.fecha, 'yyyy-MM-dd');
if (!groups[dateKey]) {
groups[dateKey] = [];
}
groups[dateKey].push(cambio);
return groups;
}, {} as Record<string, CambioMateria[]>);
const groupedHistorial = filteredHistorial.reduce(
(groups, cambio) => {
const dateKey = format(cambio.fecha, 'yyyy-MM-dd')
if (!groups[dateKey]) groups[dateKey] = []
groups[dateKey].push(cambio)
return groups
},
{} as Record<string, any[]>,
)
const sortedDates = Object.keys(groupedHistorial).sort((a, b) => b.localeCompare(a));
const sortedDates = Object.keys(groupedHistorial).sort((a, b) =>
b.localeCompare(a),
)
if (isLoading) {
return (
<div className="flex h-48 items-center justify-center">
<Loader2 className="text-primary h-8 w-8 animate-spin" />
</div>
)
}
return (
<div className="space-y-6 animate-fade-in">
<div className="animate-fade-in space-y-6">
<div className="flex items-center justify-between">
<div>
<h2 className="font-display text-2xl font-semibold text-foreground flex items-center gap-2">
<History className="w-6 h-6 text-accent" />
<h2 className="font-display text-foreground flex items-center gap-2 text-2xl font-semibold">
<History className="text-accent h-6 w-6" />
Historial de cambios
</h2>
<p className="text-sm text-muted-foreground mt-1">
{historial.length} cambios registrados
<p className="text-muted-foreground mt-1 text-sm">
{historialTransformado.length} cambios registrados
</p>
</div>
{/* Dropdown de Filtros (Igual al anterior) */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline">
<Filter className="w-4 h-4 mr-2" />
<Filter className="mr-2 h-4 w-4" />
Filtrar ({filtros.size})
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-48">
{Object.entries(tipoConfig).map(([tipo, config]) => {
const Icon = config.icon;
return (
<DropdownMenuCheckboxItem
key={tipo}
checked={filtros.has(tipo)}
onCheckedChange={() => toggleFiltro(tipo)}
>
<Icon className={cn("w-4 h-4 mr-2", config.color)} />
{config.label}
</DropdownMenuCheckboxItem>
);
})}
{Object.entries(tipoConfig).map(([tipo, config]) => (
<DropdownMenuCheckboxItem
key={tipo}
checked={filtros.has(tipo)}
onCheckedChange={() => toggleFiltro(tipo)}
>
<config.icon className={cn('mr-2 h-4 w-4', config.color)} />
{config.label}
</DropdownMenuCheckboxItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</div>
{filteredHistorial.length === 0 ? (
<Card className="card-elevated">
<Card>
<CardContent className="py-12 text-center">
<History className="w-12 h-12 mx-auto text-muted-foreground/50 mb-4" />
<p className="text-muted-foreground">
{historial.length === 0
? 'No hay cambios registrados aún'
: 'No hay cambios con los filtros seleccionados'
}
</p>
<History className="text-muted-foreground/50 mx-auto mb-4 h-12 w-12" />
<p className="text-muted-foreground">No se encontraron cambios.</p>
</CardContent>
</Card>
) : (
<div className="space-y-8">
{sortedDates.map((dateKey) => {
const cambios = groupedHistorial[dateKey];
const date = new Date(dateKey);
const isToday = format(new Date(), 'yyyy-MM-dd') === dateKey;
const isYesterday = format(new Date(Date.now() - 86400000), 'yyyy-MM-dd') === dateKey;
{sortedDates.map((dateKey) => (
<div key={dateKey}>
<div className="mb-4 flex items-center gap-3">
<Calendar className="text-muted-foreground h-4 w-4" />
<h3 className="text-foreground font-semibold">
{format(parseISO(dateKey), "EEEE, d 'de' MMMM", {
locale: es,
})}
</h3>
</div>
return (
<div key={dateKey}>
{/* Date header */}
<div className="flex items-center gap-3 mb-4">
<div className="p-2 rounded-lg bg-muted">
<Calendar className="w-4 h-4 text-muted-foreground" />
</div>
<div>
<h3 className="font-semibold text-foreground">
{isToday ? 'Hoy' : isYesterday ? 'Ayer' : format(date, "EEEE, d 'de' MMMM", { locale: es })}
</h3>
<p className="text-xs text-muted-foreground">
{cambios.length} {cambios.length === 1 ? 'cambio' : 'cambios'}
</p>
</div>
</div>
{/* Timeline */}
<div className="ml-4 border-l-2 border-border pl-6 space-y-4">
{cambios.map((cambio) => {
const config = tipoConfig[cambio.tipo];
const Icon = config.icon;
return (
<div key={cambio.id} className="relative">
{/* Timeline dot */}
<div className={cn(
"absolute -left-[31px] w-4 h-4 rounded-full border-2 border-background",
`bg-current ${config.color}`
)} />
<Card className="card-interactive">
<CardContent className="py-4">
<div className="flex items-start gap-4">
<div className={cn(
"p-2 rounded-lg bg-muted flex-shrink-0",
config.color
)}>
<Icon className="w-4 h-4" />
<div className="border-border ml-4 space-y-4 border-l-2 pl-6">
{groupedHistorial[dateKey].map((cambio) => {
const config = tipoConfig[cambio.tipo] || tipoConfig.datos
const Icon = config.icon
return (
<div key={cambio.id} className="relative">
<div
className={cn(
'border-background absolute -left-[31px] h-4 w-4 rounded-full border-2',
`bg-current ${config.color}`,
)}
/>
<Card className="card-interactive">
<CardContent className="py-4">
<div className="flex items-start gap-4">
<div
className={cn(
'bg-muted rounded-lg p-2',
config.color,
)}
>
<Icon className="h-4 w-4" />
</div>
<div className="flex-1">
<div className="flex justify-between">
<p className="font-medium">
{cambio.descripcion}
</p>
<span className="text-muted-foreground text-xs">
{format(cambio.fecha, 'HH:mm')}
</span>
</div>
<div className="flex-1 min-w-0">
<div className="flex items-start justify-between gap-2">
<div>
<p className="font-medium text-foreground">
{cambio.descripcion}
</p>
<div className="flex items-center gap-2 mt-1">
<Badge variant="outline" className="text-xs">
{config.label}
</Badge>
{cambio.detalles?.campo && (
<span className="text-xs text-muted-foreground">
Campo: {cambio.detalles.campo}
</span>
)}
</div>
</div>
<span className="text-xs text-muted-foreground whitespace-nowrap">
{format(cambio.fecha, 'HH:mm')}
</span>
</div>
<div className="flex items-center gap-2 mt-3 text-xs text-muted-foreground">
<User className="w-3 h-3" />
<span>{cambio.usuario}</span>
<span className="text-muted-foreground/50"></span>
<span>
{formatDistanceToNow(cambio.fecha, { addSuffix: true, locale: es })}
</span>
</div>
<div className="mt-2 flex items-center gap-2">
<Badge
variant="outline"
className="text-[10px]"
>
{config.label}
</Badge>
<span className="text-muted-foreground text-xs italic">
por {cambio.usuario}
</span>
</div>
</div>
</CardContent>
</Card>
</div>
);
})}
</div>
</div>
</CardContent>
</Card>
</div>
)
})}
</div>
);
})}
</div>
))}
</div>
)}
</div>
);
)
}

View File

@@ -1,4 +1,4 @@
import { useCallback, useState } from 'react'
import { useCallback, useState, useEffect } from 'react'
import { Link } from '@tanstack/react-router'
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
@@ -39,12 +39,30 @@ export interface BibliografiaTabProps {
isSaving: boolean
}
export interface AsignaturaDatos {
[key: string]: string
}
export interface AsignaturaResponse {
datos: AsignaturaDatos
}
export default function MateriaDetailPage() {
const { data: asignaturasApi, isLoading: loadingAsig } = useSubject(
'9d4dda6a-488f-428a-8a07-38081592a641',
)
// 1. Asegúrate de tener estos estados en tu componente principal
const [messages, setMessages] = useState<IAMessage[]>([])
const [datosGenerales, setDatosGenerales] = useState({})
const [campos, setCampos] = useState<CampoEstructura[]>([])
/* ---------- sincronizar API ---------- */
useEffect(() => {
if (asignaturasApi?.datos) {
setDatosGenerales(asignaturasApi.datos)
}
}, [asignaturasApi])
// 2. Funciones de manejo para la IA
const handleSendMessage = (text: string, campoId?: string) => {
const newMessage: IAMessage = {
@@ -112,17 +130,15 @@ export default function MateriaDetailPage() {
<div className="flex items-start justify-between gap-6">
<div className="space-y-3">
<Badge className="border border-blue-700 bg-blue-900/50">
IA-401
{asignaturasApi?.codigo}
</Badge>
<h1 className="text-3xl font-bold">
Inteligencia Artificial Aplicada
</h1>
<h1 className="text-3xl font-bold">{asignaturasApi?.nombre}</h1>
<div className="flex flex-wrap gap-4 text-sm text-blue-200">
<span className="flex items-center gap-1">
<GraduationCap className="h-4 w-4" />
Ingeniería en Sistemas Computacionales
{asignaturasApi?.planes_estudio?.datos?.nombre}
</span>
<span>Facultad de Ingeniería</span>
@@ -162,11 +178,14 @@ export default function MateriaDetailPage() {
{/* ================= TAB: DATOS GENERALES ================= */}
<TabsContent value="datos">
<DatosGenerales />
<DatosGenerales data={datosGenerales} isLoading={loadingAsig} />
</TabsContent>
<TabsContent value="contenido">
<ContenidoTematico></ContenidoTematico>
<ContenidoTematico
data={asignaturasApi}
isLoading={loadingAsig}
></ContenidoTematico>
</TabsContent>
<TabsContent value="bibliografia">
@@ -215,13 +234,13 @@ export default function MateriaDetailPage() {
}
/* ================= TAB CONTENT ================= */
function DatosGenerales() {
const { data: asignaturasApi, isLoading: loadingAsig } = useSubject(
/*planId*/ '9d4dda6a-488f-428a-8a07-38081592a641',
)
console.log(asignaturasApi.datos)
interface DatosGeneralesProps {
data: AsignaturaDatos
isLoading: boolean
}
function DatosGenerales({ data, isLoading }: DatosGeneralesProps) {
const formatTitle = (key: string): string =>
key.replace(/_/g, ' ').replace(/\b\w/g, (l: string) => l.toUpperCase())
return (
<div className="animate-in fade-in mx-auto max-w-7xl space-y-8 px-4 py-8 duration-500">
@@ -249,26 +268,16 @@ function DatosGenerales() {
<div className="grid grid-cols-1 gap-6 md:grid-cols-3">
{/* Columna Principal (Más ancha) */}
<div className="space-y-6 md:col-span-2">
<div className="space-y-6 md:col-span-2">
<InfoCard
title="Competencias a Desarrollar"
subtitle="Competencias profesionales que se desarrollarán"
isList={true}
initialContent={`• Diseñar algoritmos de machine learning para clasificación y predicción\n• Implementar redes neuronales profundas para procesamiento de imágenes\n• Evaluar y optimizar modelos de IA considerando métricas`}
/>
{isLoading && <p>Cargando información...</p>}
<InfoCard
title="Objetivo General"
initialContent="Formar profesionales capaces de diseñar, implementar y evaluar sistemas de inteligencia artificial que resuelvan problemas complejos."
/>
</div>
<div className="space-y-6">
<InfoCard
title="Justificación"
initialContent="La inteligencia artificial es una de las tecnologías más disruptivas..."
/>
</div>
{!isLoading &&
Object.entries(data).map(([key, value]) => (
<InfoCard
key={key}
title={formatTitle(key)}
initialContent={value}
/>
))}
</div>
{/* Columna Lateral (Información Secundaria) */}