2 Commits

30 changed files with 1774 additions and 1845 deletions

View File

@@ -8,7 +8,6 @@
"@radix-ui/react-avatar": "^1.1.11", "@radix-ui/react-avatar": "^1.1.11",
"@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-collapsible": "^1.1.12", "@radix-ui/react-collapsible": "^1.1.12",
"@radix-ui/react-context-menu": "^2.2.16",
"@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-label": "^2.1.8", "@radix-ui/react-label": "^2.1.8",
@@ -267,8 +266,6 @@
"@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="], "@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
"@radix-ui/react-context-menu": ["@radix-ui/react-context-menu@2.2.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-menu": "2.1.16", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-O8morBEW+HsVG28gYDZPTrT9UUovQUlJue5YO836tiTJhuIWBm/zQHc7j388sHWtdH/xUZurK9olD2+pcqx5ww=="],
"@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw=="], "@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw=="],
"@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw=="], "@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw=="],

View File

@@ -41,7 +41,7 @@ import { Textarea } from '@/components/ui/textarea'
import { useSubjectBibliografia } from '@/data/hooks/useSubjects' import { useSubjectBibliografia } from '@/data/hooks/useSubjects'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
// import { toast } from 'sonner'; // import { toast } from 'sonner';
// import { mockLibraryResources } from '@/data/mockAsignaturaData'; // import { mockLibraryResources } from '@/data/mockMateriaData';
export const mockLibraryResources = [ export const mockLibraryResources = [
{ {
@@ -99,7 +99,7 @@ export function BibliographyItem({
}: BibliografiaTabProps) { }: BibliografiaTabProps) {
console.log(id) console.log(id)
const { data: bibliografia2, isLoading: loadinasignatura } = const { data: bibliografia2, isLoading: loadinmateria } =
useSubjectBibliografia(id) useSubjectBibliografia(id)
const [entries, setEntries] = useState<Array<BibliografiaEntry>>(bibliografia) const [entries, setEntries] = useState<Array<BibliografiaEntry>>(bibliografia)
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false) const [isAddDialogOpen, setIsAddDialogOpen] = useState(false)

View File

@@ -23,10 +23,10 @@ import {
AlertDialogTrigger, AlertDialogTrigger,
} from '@/components/ui/alert-dialog' } from '@/components/ui/alert-dialog'
import type { import type {
DocumentoAsignatura, DocumentoMateria,
Asignatura, Materia,
AsignaturaStructure, MateriaStructure,
} from '@/types/asignatura' } from '@/types/materia'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { useSubjectBibliografia } from '@/data/hooks/useSubjects' import { useSubjectBibliografia } from '@/data/hooks/useSubjects'
//import { toast } from 'sonner'; //import { toast } from 'sonner';
@@ -34,9 +34,9 @@ import { useSubjectBibliografia } from '@/data/hooks/useSubjects'
//import { es } from 'date-fns/locale'; //import { es } from 'date-fns/locale';
interface DocumentoSEPTabProps { interface DocumentoSEPTabProps {
documento: DocumentoAsignatura | null documento: DocumentoMateria | null
asignatura: Asignatura materia: Materia
estructura: AsignaturaStructure estructura: MateriaStructure
datosGenerales: Record<string, any> datosGenerales: Record<string, any>
onRegenerate: () => void onRegenerate: () => void
isRegenerating: boolean isRegenerating: boolean
@@ -44,7 +44,7 @@ interface DocumentoSEPTabProps {
export function DocumentoSEPTab({ export function DocumentoSEPTab({
documento, documento,
asignatura, materia,
estructura, estructura,
datosGenerales, datosGenerales,
onRegenerate, onRegenerate,
@@ -112,7 +112,7 @@ export function DocumentoSEPTab({
<AlertDialogTitle>¿Regenerar documento SEP?</AlertDialogTitle> <AlertDialogTitle>¿Regenerar documento SEP?</AlertDialogTitle>
<AlertDialogDescription> <AlertDialogDescription>
Se creará una nueva versión del documento con los datos Se creará una nueva versión del documento con los datos
actuales de la asignatura. La versión anterior quedará en el actuales de la materia. La versión anterior quedará en el
historial. historial.
</AlertDialogDescription> </AlertDialogDescription>
</AlertDialogHeader> </AlertDialogHeader>
@@ -139,7 +139,7 @@ export function DocumentoSEPTab({
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<FileText className="text-primary h-5 w-5" /> <FileText className="text-primary h-5 w-5" />
<span className="text-foreground font-medium"> <span className="text-foreground font-medium">
Programa de Estudios - {asignatura.clave} Programa de Estudios - {materia.clave}
</span> </span>
</div> </div>
<Badge variant="outline">Versión {documento.version}</Badge> <Badge variant="outline">Versión {documento.version}</Badge>
@@ -155,29 +155,28 @@ export function DocumentoSEPTab({
Secretaría de Educación Pública Secretaría de Educación Pública
</p> </p>
<h1 className="font-display text-primary mb-1 text-2xl font-bold"> <h1 className="font-display text-primary mb-1 text-2xl font-bold">
{asignatura.nombre} {materia.nombre}
</h1> </h1>
<p className="text-muted-foreground text-sm"> <p className="text-muted-foreground text-sm">
Clave: {asignatura.clave} | Créditos:{' '} Clave: {materia.clave} | Créditos:{' '}
{asignatura.creditos || 'N/A'} {materia.creditos || 'N/A'}
</p> </p>
</div> </div>
{/* Datos de la institución */} {/* Datos de la institución */}
<div className="space-y-1 text-sm"> <div className="space-y-1 text-sm">
<p> <p>
<strong>Carrera:</strong> {asignatura.carrera} <strong>Carrera:</strong> {materia.carrera}
</p> </p>
<p> <p>
<strong>Facultad:</strong> {asignatura.facultad} <strong>Facultad:</strong> {materia.facultad}
</p> </p>
<p> <p>
<strong>Plan de estudios:</strong>{' '} <strong>Plan de estudios:</strong> {materia.planNombre}
{asignatura.planNombre}
</p> </p>
{asignatura.ciclo && ( {materia.ciclo && (
<p> <p>
<strong>Ciclo:</strong> {asignatura.ciclo} <strong>Ciclo:</strong> {materia.ciclo}
</p> </p>
)} )}
</div> </div>

View File

@@ -13,11 +13,7 @@ import {
} from 'lucide-react' } from 'lucide-react'
import { useState, useEffect, useRef, useMemo } from 'react' import { useState, useEffect, useRef, useMemo } from 'react'
import type { import type { IAMessage, IASugerencia, CampoEstructura } from '@/types/materia'
IAMessage,
IASugerencia,
CampoEstructura,
} from '@/types/asignatura'
import { Avatar, AvatarFallback } from '@/components/ui/avatar' import { Avatar, AvatarFallback } from '@/components/ui/avatar'
import { Badge } from '@/components/ui/badge' import { Badge } from '@/components/ui/badge'
@@ -26,7 +22,7 @@ import { ScrollArea } from '@/components/ui/scroll-area'
import { Textarea } from '@/components/ui/textarea' import { Textarea } from '@/components/ui/textarea'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
// Tipos importados de tu archivo de asignatura // Tipos importados de tu archivo de materia
const PRESETS = [ const PRESETS = [
{ {
@@ -39,7 +35,7 @@ const PRESETS = [
id: 'contenido-tematico', id: 'contenido-tematico',
label: 'Sugerir contenido', label: 'Sugerir contenido',
icon: BookOpen, icon: BookOpen,
prompt: 'Genera un desglose de temas para esta asignatura...', prompt: 'Genera un desglose de temas para esta materia...',
}, },
{ {
id: 'actividades', id: 'actividades',
@@ -61,7 +57,7 @@ interface SelectedField {
value: string value: string
} }
interface IAAsignaturaTabProps { interface IAMateriaTabProps {
campos: Array<CampoEstructura> campos: Array<CampoEstructura>
datosGenerales: Record<string, any> datosGenerales: Record<string, any>
messages: Array<IAMessage> messages: Array<IAMessage>
@@ -70,14 +66,14 @@ interface IAAsignaturaTabProps {
onRejectSuggestion: (messageId: string) => void onRejectSuggestion: (messageId: string) => void
} }
export function IAAsignaturaTab({ export function IAMateriaTab({
campos, campos,
datosGenerales, datosGenerales,
messages, messages,
onSendMessage, onSendMessage,
onAcceptSuggestion, onAcceptSuggestion,
onRejectSuggestion, onRejectSuggestion,
}: IAAsignaturaTabProps) { }: IAMateriaTabProps) {
const routerState = useRouterState() const routerState = useRouterState()
// ESTADOS PRINCIPALES (Igual que en Planes) // ESTADOS PRINCIPALES (Igual que en Planes)
@@ -87,7 +83,7 @@ export function IAAsignaturaTab({
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)
const scrollRef = useRef<HTMLDivElement>(null) const scrollRef = useRef<HTMLDivElement>(null)
// 1. Transformar datos de la asignatura para el menú // 1. Transformar datos de la materia para el menú
const availableFields = useMemo(() => { const availableFields = useMemo(() => {
// Extraemos las claves directamente del objeto datosGenerales // Extraemos las claves directamente del objeto datosGenerales
// ["nombre", "descripcion", "perfil_de_egreso", "fines_de_aprendizaje_o_formacion"] // ["nombre", "descripcion", "perfil_de_egreso", "fines_de_aprendizaje_o_formacion"]
@@ -109,7 +105,7 @@ export function IAAsignaturaTab({
}) })
}, [campos, datosGenerales]) }, [campos, datosGenerales])
// 2. Manejar el estado inicial si viene de "Datos de Asignatura" (Prefill) // 2. Manejar el estado inicial si viene de "Datos de Materia" (Prefill)
useEffect(() => { useEffect(() => {
const state = routerState.location.state as any const state = routerState.location.state as any
@@ -248,7 +244,7 @@ export function IAAsignaturaTab({
{msg.content} {msg.content}
</div> </div>
{/* Renderizado de Sugerencias (Homologado con lógica de Asignatura) */} {/* Renderizado de Sugerencias (Homologado con lógica de Materia) */}
{msg.sugerencia && !msg.sugerencia.aceptada && ( {msg.sugerencia && !msg.sugerencia.aceptada && (
<div className="animate-in fade-in slide-in-from-top-1 mt-3 w-full"> <div className="animate-in fade-in slide-in-from-top-1 mt-3 w-full">
<div className="rounded-xl border border-teal-100 bg-white p-4 shadow-md"> <div className="rounded-xl border border-teal-100 bg-white p-4 shadow-md">
@@ -306,7 +302,7 @@ export function IAAsignaturaTab({
{showSuggestions && ( {showSuggestions && (
<div className="animate-in slide-in-from-bottom-2 absolute bottom-full z-50 mb-2 w-72 overflow-hidden rounded-xl border bg-white shadow-2xl"> <div className="animate-in slide-in-from-bottom-2 absolute bottom-full z-50 mb-2 w-72 overflow-hidden rounded-xl border bg-white shadow-2xl">
<div className="border-b bg-slate-50 px-3 py-2 text-[10px] font-bold tracking-wider text-slate-500 uppercase"> <div className="border-b bg-slate-50 px-3 py-2 text-[10px] font-bold tracking-wider text-slate-500 uppercase">
Seleccionar campo de asignatura Seleccionar campo de materia
</div> </div>
<div className="max-h-64 overflow-y-auto p-1"> <div className="max-h-64 overflow-y-auto p-1">
{availableFields.map((field) => ( {availableFields.map((field) => (

View File

@@ -12,13 +12,9 @@ import { BibliographyItem } from './BibliographyItem'
import { ContenidoTematico } from './ContenidoTematico' import { ContenidoTematico } from './ContenidoTematico'
import { DocumentoSEPTab } from './DocumentoSEPTab' import { DocumentoSEPTab } from './DocumentoSEPTab'
import { HistorialTab } from './HistorialTab' import { HistorialTab } from './HistorialTab'
import { IAAsignaturaTab } from './IAAsignaturaTab' import { IAMateriaTab } from './IAMateriaTab'
import type { import type { CampoEstructura, IAMessage, IASugerencia } from '@/types/materia'
CampoEstructura,
IAMessage,
IASugerencia,
} from '@/types/asignatura'
import { Badge } from '@/components/ui/badge' import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
@@ -34,10 +30,10 @@ import {
} from '@/components/ui/tooltip' } from '@/components/ui/tooltip'
import { useSubject } from '@/data/hooks/useSubjects' import { useSubject } from '@/data/hooks/useSubjects'
import { import {
mockAsignatura, mockMateria,
mockEstructura, mockEstructura,
mockDocumentoSep, mockDocumentoSep,
} from '@/data/mockAsignaturaData' } from '@/data/mockMateriaData'
export interface BibliografiaEntry { export interface BibliografiaEntry {
id: string id: string
@@ -105,10 +101,10 @@ function EditableHeaderField({
export const Route = createFileRoute( export const Route = createFileRoute(
'/planes/$planId/asignaturas/$asignaturaId', '/planes/$planId/asignaturas/$asignaturaId',
)({ )({
component: AsignaturaDetailPage, component: MateriaDetailPage,
}) })
export default function AsignaturaDetailPage() { export default function MateriaDetailPage() {
const routerState = useRouterState() const routerState = useRouterState()
const state = routerState.location.state as any const state = routerState.location.state as any
const { asignaturaId } = useParams({ const { asignaturaId } = useParams({
@@ -125,7 +121,7 @@ export default function AsignaturaDetailPage() {
const [campos, setCampos] = useState<Array<CampoEstructura>>([]) const [campos, setCampos] = useState<Array<CampoEstructura>>([])
const [activeTab, setActiveTab] = useState('datos') const [activeTab, setActiveTab] = useState('datos')
// Dentro de AsignaturaDetailPage // Dentro de MateriaDetailPage
const [headerData, setHeaderData] = useState({ const [headerData, setHeaderData] = useState({
codigo: '', codigo: '',
nombre: '', nombre: '',
@@ -319,7 +315,7 @@ export default function AsignaturaDetailPage() {
<TabsTrigger value="datos">Datos generales</TabsTrigger> <TabsTrigger value="datos">Datos generales</TabsTrigger>
<TabsTrigger value="contenido">Contenido temático</TabsTrigger> <TabsTrigger value="contenido">Contenido temático</TabsTrigger>
<TabsTrigger value="bibliografia">Bibliografía</TabsTrigger> <TabsTrigger value="bibliografia">Bibliografía</TabsTrigger>
<TabsTrigger value="ia">IA de la asignatura</TabsTrigger> <TabsTrigger value="ia">IA de la materia</TabsTrigger>
<TabsTrigger value="sep">Documento SEP</TabsTrigger> <TabsTrigger value="sep">Documento SEP</TabsTrigger>
<TabsTrigger value="historial">Historial</TabsTrigger> <TabsTrigger value="historial">Historial</TabsTrigger>
</TabsList> </TabsList>
@@ -352,7 +348,7 @@ export default function AsignaturaDetailPage() {
</TabsContent> </TabsContent>
<TabsContent value="ia"> <TabsContent value="ia">
<IAAsignaturaTab <IAMateriaTab
campos={campos} campos={campos}
datosGenerales={datosGenerales} datosGenerales={datosGenerales}
messages={messages} messages={messages}
@@ -370,7 +366,7 @@ export default function AsignaturaDetailPage() {
<TabsContent value="sep"> <TabsContent value="sep">
<DocumentoSEPTab <DocumentoSEPTab
documento={mockDocumentoSep} documento={mockDocumentoSep}
asignatura={mockAsignatura} materia={mockMateria}
estructura={mockEstructura} estructura={mockEstructura}
datosGenerales={datosGenerales} datosGenerales={datosGenerales}
onRegenerate={handleRegenerateDocument} onRegenerate={handleRegenerateDocument}

View File

@@ -34,7 +34,7 @@ export function PasoBasicosForm({
const estructurasPlanList = catalogos?.estructurasPlan ?? [] const estructurasPlanList = catalogos?.estructurasPlan ?? []
const filteredCarreras = rawCarreras.filter((c: any) => { const filteredCarreras = rawCarreras.filter((c: any) => {
const facId = wizard.datosBasicos.facultad.id const facId = wizard.datosBasicos.facultadId
if (!facId) return true if (!facId) return true
// soportar ambos shapes: `facultad_id` (BD) o `facultadId` (local) // soportar ambos shapes: `facultad_id` (BD) o `facultadId` (local)
return c.facultad_id ? c.facultad_id === facId : c.facultadId === facId return c.facultad_id ? c.facultad_id === facId : c.facultadId === facId
@@ -68,20 +68,15 @@ export function PasoBasicosForm({
<div className="grid gap-1"> <div className="grid gap-1">
<Label htmlFor="facultad">Facultad</Label> <Label htmlFor="facultad">Facultad</Label>
<Select <Select
value={wizard.datosBasicos.facultad.id} value={wizard.datosBasicos.facultadId}
onValueChange={(value) => onValueChange={(value) =>
onChange( onChange(
(w): NewPlanWizardState => ({ (w): NewPlanWizardState => ({
...w, ...w,
datosBasicos: { datosBasicos: {
...w.datosBasicos, ...w.datosBasicos,
facultad: { facultadId: value,
id: value, carreraId: '',
nombre:
facultadesList.find((f) => f.id === value)?.nombre ||
'',
},
carrera: { id: '', nombre: '' },
}, },
}), }),
) )
@@ -91,7 +86,7 @@ export function PasoBasicosForm({
id="facultad" id="facultad"
className={cn( className={cn(
'w-full min-w-0 [&>span]:block! [&>span]:truncate!', 'w-full min-w-0 [&>span]:block! [&>span]:truncate!',
!wizard.datosBasicos.facultad.id !wizard.datosBasicos.facultadId
? 'text-muted-foreground font-normal italic opacity-70' // Es Placeholder ? 'text-muted-foreground font-normal italic opacity-70' // Es Placeholder
: 'font-medium not-italic', // Tiene Valor (Medium) : 'font-medium not-italic', // Tiene Valor (Medium)
)} )}
@@ -111,30 +106,22 @@ export function PasoBasicosForm({
<div className="grid gap-1"> <div className="grid gap-1">
<Label htmlFor="carrera">Carrera</Label> <Label htmlFor="carrera">Carrera</Label>
<Select <Select
value={wizard.datosBasicos.carrera.id} value={wizard.datosBasicos.carreraId}
onValueChange={(value) => onValueChange={(value) =>
onChange( onChange(
(w): NewPlanWizardState => ({ (w): NewPlanWizardState => ({
...w, ...w,
datosBasicos: { datosBasicos: { ...w.datosBasicos, carreraId: value },
...w.datosBasicos,
carrera: {
id: value,
nombre:
filteredCarreras.find((c) => c.id === value)?.nombre ||
'',
},
},
}), }),
) )
} }
disabled={!wizard.datosBasicos.facultad.id} disabled={!wizard.datosBasicos.facultadId}
> >
<SelectTrigger <SelectTrigger
id="carrera" id="carrera"
className={cn( className={cn(
'w-full min-w-0 [&>span]:block! [&>span]:truncate!', 'w-full min-w-0 [&>span]:block! [&>span]:truncate!',
!wizard.datosBasicos.carrera.id !wizard.datosBasicos.carreraId
? 'text-muted-foreground font-normal italic opacity-70' // Es Placeholder ? 'text-muted-foreground font-normal italic opacity-70' // Es Placeholder
: 'font-medium not-italic', // Tiene Valor (Medium) : 'font-medium not-italic', // Tiene Valor (Medium)
)} )}

View File

@@ -79,7 +79,7 @@ export function PasoDetallesPanel({
...w, ...w,
iaConfig: { iaConfig: {
...(w.iaConfig || ({} as any)), ...(w.iaConfig || ({} as any)),
instruccionesAdicionalesIA: e.target.value, InstruccionesAdicionalesIA: e.target.value,
}, },
})) }))
} }

View File

@@ -17,7 +17,6 @@ import {
TabsContents, TabsContents,
} from '@/components/ui/motion-tabs' } from '@/components/ui/motion-tabs'
import { ARCHIVOS, REPOSITORIOS } from '@/features/planes/nuevo/catalogs' import { ARCHIVOS, REPOSITORIOS } from '@/features/planes/nuevo/catalogs'
import { cn } from '@/lib/utils'
const ReferenciasParaIA = ({ const ReferenciasParaIA = ({
selectedArchivoIds = [], selectedArchivoIds = [],
@@ -88,10 +87,7 @@ const ReferenciasParaIA = ({
onCheckedChange={(checked) => onCheckedChange={(checked) =>
onToggleArchivo?.(archivo.id, !!checked) onToggleArchivo?.(archivo.id, !!checked)
} }
className={cn( className="peer border-primary ring-offset-background data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground focus-visible:ring-ring h-5 w-5 shrink-0 rounded-sm border focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50"
'peer border-primary ring-offset-background data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground focus-visible:ring-ring h-5 w-5 shrink-0 rounded-sm border focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50',
selectedArchivoIds.includes(archivo.id) ? '' : 'invisible',
)}
/> />
<FileText className="text-muted-foreground h-4 w-4" /> <FileText className="text-muted-foreground h-4 w-4" />
@@ -138,12 +134,7 @@ const ReferenciasParaIA = ({
onCheckedChange={(checked) => onCheckedChange={(checked) =>
onToggleRepositorio?.(repositorio.id, !!checked) onToggleRepositorio?.(repositorio.id, !!checked)
} }
className={cn( className="peer border-primary ring-offset-background data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground focus-visible:ring-ring h-5 w-5 shrink-0 rounded-sm border focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50"
'peer border-primary ring-offset-background data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground focus-visible:ring-ring h-5 w-5 shrink-0 rounded-sm border focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50',
selectedRepositorioIds.includes(repositorio.id)
? ''
: 'invisible',
)}
/> />
<FolderOpen className="text-muted-foreground h-4 w-4" /> <FolderOpen className="text-muted-foreground h-4 w-4" />

View File

@@ -45,8 +45,8 @@ export function PasoResumenCard({ wizard }: { wizard: NewPlanWizardState }) {
Facultad/Carrera:{' '} Facultad/Carrera:{' '}
</span> </span>
<span className="font-medium"> <span className="font-medium">
{wizard.datosBasicos.facultad.nombre || '—'} /{' '} {wizard.datosBasicos.facultadId || '—'} /{' '}
{wizard.datosBasicos.carrera.nombre || '—'} {wizard.datosBasicos.carreraId || '—'}
</span> </span>
</div> </div>
<div> <div>

View File

@@ -57,8 +57,8 @@ export function WizardControls({
const aiInput = { const aiInput = {
datosBasicos: { datosBasicos: {
nombrePlan: wizard.datosBasicos.nombrePlan, nombrePlan: wizard.datosBasicos.nombrePlan,
carreraId: wizard.datosBasicos.carrera.id || undefined, carreraId: wizard.datosBasicos.carreraId,
facultadId: wizard.datosBasicos.facultad.id || undefined, facultadId: wizard.datosBasicos.facultadId || undefined,
nivel: wizard.datosBasicos.nivel as string, nivel: wizard.datosBasicos.nivel as string,
tipoCiclo: tipoCicloSafe, tipoCiclo: tipoCicloSafe,
numCiclos: numCiclosSafe, numCiclos: numCiclosSafe,
@@ -81,7 +81,7 @@ export function WizardControls({
console.log(`${new Date().toISOString()} - Plan IA generado`, data) console.log(`${new Date().toISOString()} - Plan IA generado`, data)
navigate({ navigate({
to: `/planes/${data.plan.id}`, to: `/planes/${data.plan.id}/datos`,
state: { showConfetti: true }, state: { showConfetti: true },
}) })
return return
@@ -90,7 +90,7 @@ export function WizardControls({
if (wizard.tipoOrigen === 'MANUAL') { if (wizard.tipoOrigen === 'MANUAL') {
// Crear plan vacío manualmente usando el hook // Crear plan vacío manualmente usando el hook
const plan = await createPlanManual.mutateAsync({ const plan = await createPlanManual.mutateAsync({
carreraId: wizard.datosBasicos.carrera.id, carreraId: wizard.datosBasicos.carreraId,
estructuraId: wizard.datosBasicos.estructuraPlanId as string, estructuraId: wizard.datosBasicos.estructuraPlanId as string,
nombre: wizard.datosBasicos.nombrePlan, nombre: wizard.datosBasicos.nombrePlan,
nivel: wizard.datosBasicos.nivel as NivelPlanEstudio, nivel: wizard.datosBasicos.nivel as NivelPlanEstudio,
@@ -101,7 +101,7 @@ export function WizardControls({
// Navegar al nuevo plan // Navegar al nuevo plan
navigate({ navigate({
to: `/planes/${plan.id}`, to: `/planes/${plan.id}/datos`,
state: { showConfetti: true }, state: { showConfetti: true },
}) })
return return

View File

@@ -1,56 +1,45 @@
import type { Database } from '../types/database' import type { PostgrestError, AuthError, SupabaseClient } from "@supabase/supabase-js";
import type { import type { Database } from "../types/database";
PostgrestError,
AuthError,
SupabaseClient,
} from '@supabase/supabase-js'
export class ApiError extends Error { export class ApiError extends Error {
constructor( constructor(
message: string, message: string,
public readonly code?: string, public readonly code?: string,
public readonly details?: unknown, public readonly details?: unknown,
public readonly hint?: string, public readonly hint?: string
) { ) {
super(message) super(message);
this.name = 'ApiError' this.name = "ApiError";
} }
} }
export function throwIfError(error: PostgrestError | AuthError | null): void { export function throwIfError(error: PostgrestError | AuthError | null): void {
if (!error) return if (!error) return;
const anyErr = error as any
const anyErr = error as any;
throw new ApiError( throw new ApiError(
anyErr.message ?? 'Error inesperado', anyErr.message ?? "Error inesperado",
anyErr.code, anyErr.code,
anyErr.details, anyErr.details,
anyErr.hint, anyErr.hint
) );
} }
export function requireData<T>( export function requireData<T>(data: T | null | undefined, message = "Respuesta vacía"): T {
data: T | null | undefined, if (data === null || data === undefined) throw new ApiError(message);
message = 'Respuesta vacía', return data;
): T {
if (data === null || data === undefined) throw new ApiError(message)
return data
} }
export async function getUserIdOrThrow( export async function getUserIdOrThrow(supabase: SupabaseClient<Database>): Promise<string> {
supabase: SupabaseClient<Database>, const { data, error } = await supabase.auth.getUser();
): Promise<string> { throwIfError(error);
const { data, error } = await supabase.auth.getUser() if (!data?.user?.id) throw new ApiError("No hay sesión activa (auth).");
throwIfError(error) return data.user.id;
if (!data?.user?.id) throw new ApiError('No hay sesión activa (auth).')
return data.user.id
} }
export function buildRange( export function buildRange(limit?: number, offset?: number): { from?: number; to?: number } {
limit?: number, if (!limit) return {};
offset?: number, const from = Math.max(0, offset ?? 0);
): { from?: number; to?: number } { const to = from + Math.max(1, limit) - 1;
if (!limit) return {} return { from, to };
const from = Math.max(0, offset ?? 0)
const to = from + Math.max(1, limit) - 1
return { from, to }
} }

View File

@@ -1,32 +1,32 @@
import { invokeEdge } from '../supabase/invokeEdge' import { invokeEdge } from "../supabase/invokeEdge";
import type { UUID } from '../types/domain' import type { UUID } from "../types/domain";
/** /**
* Metadata “canónica” para UI (archivo OpenAI + espejo en Supabase) * Metadata “canónica” para UI (archivo OpenAI + espejo en Supabase)
* Se apoya en tu tabla `archivos`. * Se apoya en tu tabla `archivos`.
*/ */
export type AppFile = { export type AppFile = {
id: UUID // id interno (tabla archivos) id: UUID; // id interno (tabla archivos)
openai_file_id: string // id OpenAI openai_file_id: string; // id OpenAI
nombre: string nombre: string;
mime_type: string | null mime_type: string | null;
bytes: number | null bytes: number | null;
// espejo Supabase para preview/descarga // espejo Supabase para preview/descarga
ruta_storage: string | null // "bucket/path" ruta_storage: string | null; // "bucket/path"
signed_url?: string | null signed_url?: string | null;
// auditoría/evidencia // auditoría/evidencia
temporal: boolean temporal: boolean;
notas?: string | null notas?: string | null;
subido_en: string subido_en: string;
} };
const EDGE = { const EDGE = {
upload: 'openai_files_upload', upload: "openai_files_upload",
remove: 'openai_files_delete', remove: "openai_files_delete",
} as const } as const;
/** /**
* Sube archivo a OpenAI y (opcional) crea espejo en Storage * Sube archivo a OpenAI y (opcional) crea espejo en Storage
@@ -37,28 +37,28 @@ export async function openai_files_upload(payload: {
* Si tu Edge soporta multipart: manda File/Blob directo. * Si tu Edge soporta multipart: manda File/Blob directo.
* Si no, manda base64/bytes (según tu implementación). * Si no, manda base64/bytes (según tu implementación).
*/ */
file: File file: File;
/** “temporal” = evidencia usada para generar plan/asignatura */ /** “temporal” = evidencia usada para generar plan/materia */
temporal?: boolean temporal?: boolean;
/** contexto para auditoría */ /** contexto para auditoría */
contexto?: { contexto?: {
planId?: UUID planId?: UUID;
asignaturaId?: UUID asignaturaId?: UUID;
motivo?: 'WIZARD_PLAN' | 'WIZARD_MATERIA' | 'ADHOC' motivo?: "WIZARD_PLAN" | "WIZARD_MATERIA" | "ADHOC";
} };
/** si quieres forzar espejo para preview siempre */ /** si quieres forzar espejo para preview siempre */
mirrorToSupabase?: boolean mirrorToSupabase?: boolean;
}): Promise<AppFile> { }): Promise<AppFile> {
return invokeEdge<AppFile>(EDGE.upload, payload) return invokeEdge<AppFile>(EDGE.upload, payload);
} }
export async function openai_files_delete(payload: { export async function openai_files_delete(payload: {
openaiFileId: string openaiFileId: string;
/** si quieres borrar también espejo y registro */ /** si quieres borrar también espejo y registro */
hardDelete?: boolean hardDelete?: boolean;
}): Promise<{ ok: true }> { }): Promise<{ ok: true }> {
return invokeEdge<{ ok: true }>(EDGE.remove, payload) return invokeEdge<{ ok: true }>(EDGE.remove, payload);
} }

View File

@@ -165,7 +165,7 @@ export async function plan_asignaturas_list(
const { data, error } = await supabase const { data, error } = await supabase
.from('asignaturas') .from('asignaturas')
.select( .select(
'id,plan_estudio_id,estructura_id,codigo,nombre,tipo,creditos,horas_independientes,horas_academicas,numero_ciclo,linea_plan_id,orden_celda,datos,contenido_tematico,tipo_origen,meta_origen,creado_por,actualizado_por,creado_en,actualizado_en', 'id,plan_estudio_id,estructura_id,facultad_propietaria_id,codigo,nombre,tipo,creditos,horas_semana,numero_ciclo,linea_plan_id,orden_celda,datos,contenido_tematico,tipo_origen,meta_origen,creado_por,actualizado_por,creado_en,actualizado_en',
) )
.eq('plan_estudio_id', planId) .eq('plan_estudio_id', planId)
.order('numero_ciclo', { ascending: true, nullsFirst: false }) .order('numero_ciclo', { ascending: true, nullsFirst: false })
@@ -333,7 +333,7 @@ export async function plans_import_from_files(payload: {
} }
archivoWordPlanId: UUID archivoWordPlanId: UUID
archivoMapaExcelId?: UUID | null archivoMapaExcelId?: UUID | null
archivoAsignaturasExcelId?: UUID | null archivoMateriasExcelId?: UUID | null
}): Promise<PlanEstudio> { }): Promise<PlanEstudio> {
return invokeEdge<PlanEstudio>(EDGE.plans_import_from_files, payload) return invokeEdge<PlanEstudio>(EDGE.plans_import_from_files, payload)
} }

View File

@@ -1,209 +1,181 @@
import { supabaseBrowser } from '../supabase/client' import { supabaseBrowser } from "../supabase/client";
import { invokeEdge } from '../supabase/invokeEdge' import { invokeEdge } from "../supabase/invokeEdge";
import { throwIfError, requireData } from "./_helpers";
import { throwIfError, requireData } from './_helpers'
import type { DocumentoResult } from './plans.api'
import type { import type {
Asignatura, Asignatura,
BibliografiaAsignatura, BibliografiaAsignatura,
CambioAsignatura, CambioAsignatura,
TipoAsignatura, TipoAsignatura,
UUID, UUID,
} from '../types/domain' } from "../types/domain";
import type { DocumentoResult } from "./plans.api";
const EDGE = { const EDGE = {
subjects_create_manual: 'subjects_create_manual', subjects_create_manual: "subjects_create_manual",
ai_generate_subject: 'ai_generate_subject', ai_generate_subject: "ai_generate_subject",
subjects_persist_from_ai: 'subjects_persist_from_ai', subjects_persist_from_ai: "subjects_persist_from_ai",
subjects_clone_from_existing: 'subjects_clone_from_existing', subjects_clone_from_existing: "subjects_clone_from_existing",
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_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 async function subjects_get(subjectId: UUID): Promise<Asignatura> { export async function subjects_get(subjectId: UUID): Promise<Asignatura> {
const supabase = supabaseBrowser() const supabase = supabaseBrowser();
const { data, error } = await supabase const { data, error } = await supabase
.from('asignaturas') .from("asignaturas")
.select( .select(
` `
id,plan_estudio_id,estructura_id,codigo,nombre,tipo,creditos,horas_independientes,horas_academicas,numero_ciclo,linea_plan_id,orden_celda,datos,contenido_tematico,tipo_origen,meta_origen,creado_por,actualizado_por,creado_en,actualizado_en, id,plan_estudio_id,estructura_id,facultad_propietaria_id,codigo,nombre,tipo,creditos,horas_semana,numero_ciclo,linea_plan_id,orden_celda,datos,contenido_tematico,tipo_origen,meta_origen,creado_por,actualizado_por,creado_en,actualizado_en,
planes_estudio( planes_estudio(
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, 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(id,facultad_id,nombre,nombre_corto,clave_sep,activa, facultades(id,nombre,nombre_corto,color,icono)) carreras(id,facultad_id,nombre,nombre_corto,clave_sep,activa, facultades(id,nombre,nombre_corto,color,icono))
), ),
estructuras_asignatura(id,nombre,version,definicion) estructuras_asignatura(id,nombre,version,definicion)
`, `
) )
.eq('id', subjectId) .eq("id", subjectId)
.single() .single();
throwIfError(error) throwIfError(error);
return requireData(data, 'Asignatura no encontrada.') return requireData(data, "Materia no encontrada.");
} }
export async function subjects_history( export async function subjects_history(subjectId: UUID): Promise<CambioAsignatura[]> {
subjectId: UUID, const supabase = supabaseBrowser();
): Promise<Array<CambioAsignatura>> {
const supabase = supabaseBrowser()
const { data, error } = await supabase const { data, error } = await supabase
.from('cambios_asignatura') .from("cambios_asignatura")
.select( .select(
'id,asignatura_id,cambiado_por,cambiado_en,tipo,campo,valor_anterior,valor_nuevo,fuente,interaccion_ia_id', "id,asignatura_id,cambiado_por,cambiado_en,tipo,campo,valor_anterior,valor_nuevo,fuente,interaccion_ia_id"
) )
.eq('asignatura_id', subjectId) .eq("asignatura_id", subjectId)
.order('cambiado_en', { ascending: false }) .order("cambiado_en", { ascending: false });
throwIfError(error) throwIfError(error);
return data ?? [] return data ?? [];
} }
export async function subjects_bibliografia_list( export async function subjects_bibliografia_list(subjectId: UUID): Promise<BibliografiaAsignatura[]> {
subjectId: UUID, const supabase = supabaseBrowser();
): Promise<Array<BibliografiaAsignatura>> {
const supabase = supabaseBrowser()
const { data, error } = await supabase const { data, error } = await supabase
.from('bibliografia_asignatura') .from("bibliografia_asignatura")
.select( .select("id,asignatura_id,tipo,cita,tipo_fuente,biblioteca_item_id,creado_por,creado_en,actualizado_en")
'id,asignatura_id,tipo,cita,tipo_fuente,biblioteca_item_id,creado_por,creado_en,actualizado_en', .eq("asignatura_id", subjectId)
) .order("tipo", { ascending: true })
.eq('asignatura_id', subjectId) .order("creado_en", { ascending: true });
.order('tipo', { ascending: true })
.order('creado_en', { ascending: true })
throwIfError(error) throwIfError(error);
return data ?? [] return data ?? [];
} }
/** Wizard: crear asignatura manual (Edge Function) */ /** Wizard: crear materia manual (Edge Function) */
export type SubjectsCreateManualInput = { export type SubjectsCreateManualInput = {
planId: UUID planId: UUID;
datosBasicos: { datosBasicos: {
nombre: string nombre: string;
clave?: string clave?: string;
tipo: TipoAsignatura tipo: TipoAsignatura;
creditos: number creditos: number;
horasSemana?: number horasSemana?: number;
estructuraId: UUID estructuraId: UUID;
} };
} };
export async function subjects_create_manual( export async function subjects_create_manual(payload: SubjectsCreateManualInput): Promise<Asignatura> {
payload: SubjectsCreateManualInput, return invokeEdge<Asignatura>(EDGE.subjects_create_manual, payload);
): Promise<Asignatura> {
return invokeEdge<Asignatura>(EDGE.subjects_create_manual, payload)
} }
export async function ai_generate_subject(payload: { export async function ai_generate_subject(payload: {
planId: UUID planId: UUID;
datosBasicos: { datosBasicos: {
nombre: string nombre: string;
clave?: string clave?: string;
tipo: TipoAsignatura tipo: TipoAsignatura;
creditos: number creditos: number;
horasSemana?: number horasSemana?: number;
estructuraId: UUID estructuraId: UUID;
} };
iaConfig: { iaConfig: {
descripcionEnfoque: string descripcionEnfoque: string;
notasAdicionales?: string notasAdicionales?: string;
archivosExistentesIds?: Array<UUID> archivosExistentesIds?: UUID[];
repositoriosIds?: Array<UUID> repositoriosIds?: UUID[];
archivosAdhocIds?: Array<UUID> archivosAdhocIds?: UUID[];
usarMCP?: boolean usarMCP?: boolean;
} };
}): Promise<any> { }): Promise<any> {
return invokeEdge<any>(EDGE.ai_generate_subject, payload) return invokeEdge<any>(EDGE.ai_generate_subject, payload);
} }
export async function subjects_persist_from_ai(payload: { export async function subjects_persist_from_ai(payload: { planId: UUID; jsonMateria: any }): Promise<Asignatura> {
planId: UUID return invokeEdge<Asignatura>(EDGE.subjects_persist_from_ai, payload);
jsonAsignatura: any
}): Promise<Asignatura> {
return invokeEdge<Asignatura>(EDGE.subjects_persist_from_ai, payload)
} }
export async function subjects_clone_from_existing(payload: { export async function subjects_clone_from_existing(payload: {
asignaturaOrigenId: UUID materiaOrigenId: UUID;
planDestinoId: UUID planDestinoId: UUID;
overrides?: Partial<{ overrides?: Partial<{
nombre: string nombre: string;
codigo: string codigo: string;
tipo: TipoAsignatura tipo: TipoAsignatura;
creditos: number creditos: number;
horas_semana: number horas_semana: number;
}> }>;
}): Promise<Asignatura> { }): Promise<Asignatura> {
return invokeEdge<Asignatura>(EDGE.subjects_clone_from_existing, payload) return invokeEdge<Asignatura>(EDGE.subjects_clone_from_existing, payload);
} }
export async function subjects_import_from_file(payload: { export async function subjects_import_from_file(payload: {
planId: UUID planId: UUID;
archivoWordAsignaturaId: UUID archivoWordMateriaId: UUID;
archivosAdicionalesIds?: Array<UUID> archivosAdicionalesIds?: UUID[];
}): Promise<Asignatura> { }): Promise<Asignatura> {
return invokeEdge<Asignatura>(EDGE.subjects_import_from_file, payload) return invokeEdge<Asignatura>(EDGE.subjects_import_from_file, payload);
} }
/** Guardado de tarjetas/fields (Edge: merge server-side en asignaturas.datos y columnas) */ /** Guardado de tarjetas/fields (Edge: merge server-side en asignaturas.datos y columnas) */
export type SubjectsUpdateFieldsPatch = Partial<{ export type SubjectsUpdateFieldsPatch = Partial<{
codigo: string | null codigo: string | null;
nombre: string nombre: string;
tipo: TipoAsignatura tipo: TipoAsignatura;
creditos: number creditos: number;
horas_semana: number | null horas_semana: number | null;
numero_ciclo: number | null numero_ciclo: number | null;
linea_plan_id: UUID | null linea_plan_id: UUID | null;
datos: Record<string, any> datos: Record<string, any>;
}> }>;
export async function subjects_update_fields( export async function subjects_update_fields(subjectId: UUID, patch: SubjectsUpdateFieldsPatch): Promise<Asignatura> {
subjectId: UUID, return invokeEdge<Asignatura>(EDGE.subjects_update_fields, { subjectId, patch });
patch: SubjectsUpdateFieldsPatch,
): Promise<Asignatura> {
return invokeEdge<Asignatura>(EDGE.subjects_update_fields, {
subjectId,
patch,
})
} }
export async function subjects_update_contenido( export async function subjects_update_contenido(subjectId: UUID, unidades: any[]): Promise<Asignatura> {
subjectId: UUID, return invokeEdge<Asignatura>(EDGE.subjects_update_contenido, { subjectId, unidades });
unidades: Array<any>,
): Promise<Asignatura> {
return invokeEdge<Asignatura>(EDGE.subjects_update_contenido, {
subjectId,
unidades,
})
} }
export type BibliografiaUpsertInput = Array<{ export type BibliografiaUpsertInput = Array<{
id?: UUID id?: UUID;
tipo: 'BASICA' | 'COMPLEMENTARIA' tipo: "BASICA" | "COMPLEMENTARIA";
cita: string cita: string;
tipo_fuente?: 'MANUAL' | 'BIBLIOTECA' tipo_fuente?: "MANUAL" | "BIBLIOTECA";
biblioteca_item_id?: string | null biblioteca_item_id?: string | null;
}> }>;
export async function subjects_update_bibliografia( export async function subjects_update_bibliografia(
subjectId: UUID, subjectId: UUID,
entries: BibliografiaUpsertInput, entries: BibliografiaUpsertInput
): Promise<{ ok: true }> { ): Promise<{ ok: true }> {
return invokeEdge<{ ok: true }>(EDGE.subjects_update_bibliografia, { return invokeEdge<{ ok: true }>(EDGE.subjects_update_bibliografia, { subjectId, entries });
subjectId,
entries,
})
} }
/** Documento SEP asignatura */ /** Documento SEP materia */
/* export type DocumentoResult = { /* export type DocumentoResult = {
archivoId: UUID; archivoId: UUID;
signedUrl: string; signedUrl: string;
@@ -211,18 +183,10 @@ export async function subjects_update_bibliografia(
nombre?: string; nombre?: string;
}; */ }; */
export async function subjects_generate_document( export async function subjects_generate_document(subjectId: UUID): Promise<DocumentoResult> {
subjectId: UUID, return invokeEdge<DocumentoResult>(EDGE.subjects_generate_document, { subjectId });
): Promise<DocumentoResult> {
return invokeEdge<DocumentoResult>(EDGE.subjects_generate_document, {
subjectId,
})
} }
export async function subjects_get_document( export async function subjects_get_document(subjectId: UUID): Promise<DocumentoResult | null> {
subjectId: UUID, return invokeEdge<DocumentoResult | null>(EDGE.subjects_get_document, { subjectId });
): Promise<DocumentoResult | null> {
return invokeEdge<DocumentoResult | null>(EDGE.subjects_get_document, {
subjectId,
})
} }

View File

@@ -91,7 +91,7 @@ export function usePersistSubjectFromAI() {
const qc = useQueryClient() const qc = useQueryClient()
return useMutation({ return useMutation({
mutationFn: (payload: { planId: UUID; jsonAsignatura: any }) => mutationFn: (payload: { planId: UUID; jsonMateria: any }) =>
subjects_persist_from_ai(payload), subjects_persist_from_ai(payload),
onSuccess: (subject) => { onSuccess: (subject) => {
qc.setQueryData(qk.asignatura(subject.id), subject) qc.setQueryData(qk.asignatura(subject.id), subject)

View File

@@ -1,14 +1,14 @@
import type { import type {
Asignatura, Materia,
AsignaturaStructure, MateriaStructure,
UnidadTematica, UnidadTematica,
BibliografiaEntry, BibliografiaEntry,
CambioAsignatura, CambioMateria,
DocumentoAsignatura, DocumentoMateria,
LibraryResource, LibraryResource
} from '@/types/asignatura' } from '@/types/materia';
export const mockAsignatura: Asignatura = { export const mockMateria: Materia = {
id: '1', id: '1',
nombre: 'Inteligencia Artificial Aplicada', nombre: 'Inteligencia Artificial Aplicada',
clave: 'IAA-401', clave: 'IAA-401',
@@ -20,9 +20,9 @@ export const mockAsignatura: Asignatura = {
carrera: 'Ingeniería en Sistemas Computacionales', carrera: 'Ingeniería en Sistemas Computacionales',
facultad: 'Facultad de Ingeniería', facultad: 'Facultad de Ingeniería',
estructuraId: 'estructura-1', estructuraId: 'estructura-1',
} };
export const mockEstructura: AsignaturaStructure = { export const mockEstructura: MateriaStructure = {
id: 'estructura-1', id: 'estructura-1',
nombre: 'Plantilla SEP Licenciatura', nombre: 'Plantilla SEP Licenciatura',
campos: [ campos: [
@@ -31,7 +31,7 @@ export const mockEstructura: AsignaturaStructure = {
nombre: 'Objetivo General', nombre: 'Objetivo General',
tipo: 'texto_largo', tipo: 'texto_largo',
obligatorio: true, obligatorio: true,
descripcion: 'Describe el propósito principal de la asignatura', descripcion: 'Describe el propósito principal de la materia',
placeholder: 'Al finalizar el curso, el estudiante será capaz de...', placeholder: 'Al finalizar el curso, el estudiante será capaz de...',
}, },
{ {
@@ -46,14 +46,14 @@ export const mockEstructura: AsignaturaStructure = {
nombre: 'Justificación', nombre: 'Justificación',
tipo: 'texto_largo', tipo: 'texto_largo',
obligatorio: true, obligatorio: true,
descripcion: 'Relevancia de la asignatura en el plan de estudios', descripcion: 'Relevancia de la materia en el plan de estudios',
}, },
{ {
id: 'requisitos', id: 'requisitos',
nombre: 'Requisitos / Seriación', nombre: 'Requisitos / Seriación',
tipo: 'texto', tipo: 'texto',
obligatorio: false, obligatorio: false,
descripcion: 'Asignaturas previas requeridas', descripcion: 'Materias previas requeridas',
}, },
{ {
id: 'estrategias_didacticas', id: 'estrategias_didacticas',
@@ -77,49 +77,27 @@ export const mockEstructura: AsignaturaStructure = {
descripcion: 'Características requeridas del profesor', descripcion: 'Características requeridas del profesor',
}, },
], ],
} };
export const mockDatosGenerales: Record<string, any> = { export const mockDatosGenerales: Record<string, any> = {
objetivo_general: objetivo_general: 'Formar profesionales capaces de diseñar, implementar y evaluar sistemas de inteligencia artificial que resuelvan problemas complejos del mundo real, aplicando principios éticos y metodologías actuales en el campo.',
'Formar profesionales capaces de diseñar, implementar y evaluar sistemas de inteligencia artificial que resuelvan problemas complejos del mundo real, aplicando principios éticos y metodologías actuales en el campo.', competencias: '• Diseñar algoritmos de machine learning para clasificación y predicción\n• Implementar redes neuronales profundas para procesamiento de imágenes y texto\n• Evaluar y optimizar modelos de IA considerando métricas de rendimiento\n• Aplicar principios éticos en el desarrollo de sistemas inteligentes',
competencias: justificacion: 'La inteligencia artificial es una de las tecnologías más disruptivas del siglo XXI. Su integración en diversos sectores demanda profesionales con sólidas bases teóricas y prácticas. Esta materia proporciona las competencias necesarias para que el egresado pueda innovar y contribuir al desarrollo tecnológico del país.',
'• Diseñar algoritmos de machine learning para clasificación y predicción\n• Implementar redes neuronales profundas para procesamiento de imágenes y texto\n• Evaluar y optimizar modelos de IA considerando métricas de rendimiento\n• Aplicar principios éticos en el desarrollo de sistemas inteligentes', requisitos: 'Programación Avanzada (PAV-301), Matemáticas Discretas (MAT-201)',
justificacion: estrategias_didacticas: '• Aprendizaje basado en proyectos\n• Talleres prácticos con datasets reales\n• Exposiciones y discusiones grupales\n• Análisis de casos de estudio\n• Desarrollo de prototipo integrador',
'La inteligencia artificial es una de las tecnologías más disruptivas del siglo XXI. Su integración en diversos sectores demanda profesionales con sólidas bases teóricas y prácticas. Esta asignatura proporciona las competencias necesarias para que el egresado pueda innovar y contribuir al desarrollo tecnológico del país.', evaluacion: '• Exámenes parciales: 30%\n• Proyecto integrador: 35%\n• Prácticas de laboratorio: 20%\n• Participación y tareas: 15%',
requisitos: perfil_docente: 'Profesional con maestría o doctorado en áreas afines a la inteligencia artificial, con experiencia mínima de 3 años en docencia y desarrollo de proyectos de IA.',
'Programación Avanzada (PAV-301), Matemáticas Discretas (MAT-201)', };
estrategias_didacticas:
'• Aprendizaje basado en proyectos\n• Talleres prácticos con datasets reales\n• Exposiciones y discusiones grupales\n• Análisis de casos de estudio\n• Desarrollo de prototipo integrador',
evaluacion:
'• Exámenes parciales: 30%\n• Proyecto integrador: 35%\n• Prácticas de laboratorio: 20%\n• Participación y tareas: 15%',
perfil_docente:
'Profesional con maestría o doctorado en áreas afines a la inteligencia artificial, con experiencia mínima de 3 años en docencia y desarrollo de proyectos de IA.',
}
export const mockContenidoTematico: Array<UnidadTematica> = [ export const mockContenidoTematico: UnidadTematica[] = [
{ {
id: 'unidad-1', id: 'unidad-1',
nombre: 'Fundamentos de Inteligencia Artificial', nombre: 'Fundamentos de Inteligencia Artificial',
numero: 1, numero: 1,
temas: [ temas: [
{ { id: 'tema-1-1', nombre: 'Historia y evolución de la IA', descripcion: 'Desde los orígenes hasta la actualidad', horasEstimadas: 2 },
id: 'tema-1-1', { id: 'tema-1-2', nombre: 'Tipos de IA y aplicaciones', descripcion: 'IA débil, fuerte y superinteligencia', horasEstimadas: 3 },
nombre: 'Historia y evolución de la IA', { id: 'tema-1-3', nombre: 'Ética en IA', descripcion: 'Consideraciones éticas y responsabilidad', horasEstimadas: 2 },
descripcion: 'Desde los orígenes hasta la actualidad',
horasEstimadas: 2,
},
{
id: 'tema-1-2',
nombre: 'Tipos de IA y aplicaciones',
descripcion: 'IA débil, fuerte y superinteligencia',
horasEstimadas: 3,
},
{
id: 'tema-1-3',
nombre: 'Ética en IA',
descripcion: 'Consideraciones éticas y responsabilidad',
horasEstimadas: 2,
},
], ],
}, },
{ {
@@ -127,24 +105,9 @@ export const mockContenidoTematico: Array<UnidadTematica> = [
nombre: 'Machine Learning', nombre: 'Machine Learning',
numero: 2, numero: 2,
temas: [ temas: [
{ { id: 'tema-2-1', nombre: 'Aprendizaje supervisado', descripcion: 'Regresión y clasificación', horasEstimadas: 6 },
id: 'tema-2-1', { id: 'tema-2-2', nombre: 'Aprendizaje no supervisado', descripcion: 'Clustering y reducción de dimensionalidad', horasEstimadas: 5 },
nombre: 'Aprendizaje supervisado', { id: 'tema-2-3', nombre: 'Evaluación de modelos', descripcion: 'Métricas y validación cruzada', horasEstimadas: 4 },
descripcion: 'Regresión y clasificación',
horasEstimadas: 6,
},
{
id: 'tema-2-2',
nombre: 'Aprendizaje no supervisado',
descripcion: 'Clustering y reducción de dimensionalidad',
horasEstimadas: 5,
},
{
id: 'tema-2-3',
nombre: 'Evaluación de modelos',
descripcion: 'Métricas y validación cruzada',
horasEstimadas: 4,
},
], ],
}, },
{ {
@@ -152,30 +115,10 @@ export const mockContenidoTematico: Array<UnidadTematica> = [
nombre: 'Deep Learning', nombre: 'Deep Learning',
numero: 3, numero: 3,
temas: [ temas: [
{ { id: 'tema-3-1', nombre: 'Redes neuronales artificiales', descripcion: 'Perceptrón y backpropagation', horasEstimadas: 5 },
id: 'tema-3-1', { id: 'tema-3-2', nombre: 'Redes convolucionales (CNN)', descripcion: 'Procesamiento de imágenes', horasEstimadas: 6 },
nombre: 'Redes neuronales artificiales', { id: 'tema-3-3', nombre: 'Redes recurrentes (RNN)', descripcion: 'Procesamiento de secuencias', horasEstimadas: 5 },
descripcion: 'Perceptrón y backpropagation', { id: 'tema-3-4', nombre: 'Transformers y atención', descripcion: 'Arquitecturas modernas', horasEstimadas: 6 },
horasEstimadas: 5,
},
{
id: 'tema-3-2',
nombre: 'Redes convolucionales (CNN)',
descripcion: 'Procesamiento de imágenes',
horasEstimadas: 6,
},
{
id: 'tema-3-3',
nombre: 'Redes recurrentes (RNN)',
descripcion: 'Procesamiento de secuencias',
horasEstimadas: 5,
},
{
id: 'tema-3-4',
nombre: 'Transformers y atención',
descripcion: 'Arquitecturas modernas',
horasEstimadas: 6,
},
], ],
}, },
{ {
@@ -183,29 +126,14 @@ export const mockContenidoTematico: Array<UnidadTematica> = [
nombre: 'Aplicaciones Prácticas', nombre: 'Aplicaciones Prácticas',
numero: 4, numero: 4,
temas: [ temas: [
{ { id: 'tema-4-1', nombre: 'Procesamiento de lenguaje natural', descripcion: 'NLP y chatbots', horasEstimadas: 6 },
id: 'tema-4-1', { id: 'tema-4-2', nombre: 'Visión por computadora', descripcion: 'Detección y reconocimiento', horasEstimadas: 5 },
nombre: 'Procesamiento de lenguaje natural', { id: 'tema-4-3', nombre: 'Sistemas de recomendación', descripcion: 'Filtrado colaborativo y contenido', horasEstimadas: 4 },
descripcion: 'NLP y chatbots',
horasEstimadas: 6,
},
{
id: 'tema-4-2',
nombre: 'Visión por computadora',
descripcion: 'Detección y reconocimiento',
horasEstimadas: 5,
},
{
id: 'tema-4-3',
nombre: 'Sistemas de recomendación',
descripcion: 'Filtrado colaborativo y contenido',
horasEstimadas: 4,
},
], ],
}, },
] ];
export const mockBibliografia: Array<BibliografiaEntry> = [ export const mockBibliografia: BibliografiaEntry[] = [
{ {
id: 'bib-1', id: 'bib-1',
tipo: 'BASICA', tipo: 'BASICA',
@@ -225,14 +153,13 @@ export const mockBibliografia: Array<BibliografiaEntry> = [
{ {
id: 'bib-2', id: 'bib-2',
tipo: 'BASICA', tipo: 'BASICA',
cita: "Géron, A. (2022). Hands-On Machine Learning with Scikit-Learn, Keras, and TensorFlow (3rd ed.). O'Reilly Media.", cita: 'Géron, A. (2022). Hands-On Machine Learning with Scikit-Learn, Keras, and TensorFlow (3rd ed.). O\'Reilly Media.',
fuenteBibliotecaId: 'lib-2', fuenteBibliotecaId: 'lib-2',
fuenteBiblioteca: { fuenteBiblioteca: {
id: 'lib-2', id: 'lib-2',
titulo: titulo: 'Hands-On Machine Learning with Scikit-Learn, Keras, and TensorFlow',
'Hands-On Machine Learning with Scikit-Learn, Keras, and TensorFlow',
autor: 'Aurélien Géron', autor: 'Aurélien Géron',
editorial: "O'Reilly Media", editorial: 'O\'Reilly Media',
anio: 2022, anio: 2022,
isbn: '978-1098125974', isbn: '978-1098125974',
tipo: 'libro', tipo: 'libro',
@@ -260,9 +187,9 @@ export const mockBibliografia: Array<BibliografiaEntry> = [
disponible: false, disponible: false,
}, },
}, },
] ];
export const mockHistorial: Array<CambioAsignatura> = [ export const mockHistorial: CambioMateria[] = [
{ {
id: 'cambio-1', id: 'cambio-1',
tipo: 'datos', tipo: 'datos',
@@ -301,17 +228,17 @@ export const mockHistorial: Array<CambioAsignatura> = [
usuario: 'Sistema', usuario: 'Sistema',
fecha: new Date('2024-12-06T11:30:00'), fecha: new Date('2024-12-06T11:30:00'),
}, },
] ];
export const mockDocumentoSep: DocumentoAsignatura = { export const mockDocumentoSep: DocumentoMateria = {
id: 'doc-1', id: 'doc-1',
asignaturaId: '1', materiaId: '1',
version: 3, version: 3,
fechaGeneracion: new Date('2024-12-06T11:30:00'), fechaGeneracion: new Date('2024-12-06T11:30:00'),
estado: 'listo', estado: 'listo',
} };
export const mockLibraryResources: Array<LibraryResource> = [ export const mockLibraryResources: LibraryResource[] = [
{ {
id: 'lib-1', id: 'lib-1',
titulo: 'Artificial Intelligence: A Modern Approach', titulo: 'Artificial Intelligence: A Modern Approach',
@@ -324,10 +251,9 @@ export const mockLibraryResources: Array<LibraryResource> = [
}, },
{ {
id: 'lib-2', id: 'lib-2',
titulo: titulo: 'Hands-On Machine Learning with Scikit-Learn, Keras, and TensorFlow',
'Hands-On Machine Learning with Scikit-Learn, Keras, and TensorFlow',
autor: 'Aurélien Géron', autor: 'Aurélien Géron',
editorial: "O'Reilly Media", editorial: 'O\'Reilly Media',
anio: 2022, anio: 2022,
isbn: '978-1098125974', isbn: '978-1098125974',
tipo: 'libro', tipo: 'libro',
@@ -373,4 +299,4 @@ export const mockLibraryResources: Array<LibraryResource> = [
tipo: 'libro', tipo: 'libro',
disponible: true, disponible: true,
}, },
] ];

View File

@@ -1,156 +1,156 @@
import type { NivelPlanEstudio, TipoCiclo } from '@/data/types/domain' import type { NivelPlanEstudio, TipoCiclo } from "@/data/types/domain";
export const FACULTADES = [ export const FACULTADES = [
{ id: 'ing', nombre: 'Facultad de Ingeniería' }, { id: "ing", nombre: "Facultad de Ingeniería" },
{ {
id: 'med', id: "med",
nombre: 'Facultad de Medicina en medicina en medicina en medicina', nombre: "Facultad de Medicina en medicina en medicina en medicina",
}, },
{ id: 'neg', nombre: 'Facultad de Negocios' }, { id: "neg", nombre: "Facultad de Negocios" },
] ];
export const CARRERAS = [ export const CARRERAS = [
{ id: 'sis', nombre: 'Ing. en Sistemas', facultadId: 'ing' }, { id: "sis", nombre: "Ing. en Sistemas", facultadId: "ing" },
{ id: 'ind', nombre: 'Ing. Industrial', facultadId: 'ing' }, { id: "ind", nombre: "Ing. Industrial", facultadId: "ing" },
{ id: 'medico', nombre: 'Médico Cirujano', facultadId: 'med' }, { id: "medico", nombre: "Médico Cirujano", facultadId: "med" },
{ id: 'act', nombre: 'Actuaría', facultadId: 'neg' }, { id: "act", nombre: "Actuaría", facultadId: "neg" },
] ];
export const NIVELES: Array<NivelPlanEstudio> = [ export const NIVELES: Array<NivelPlanEstudio> = [
'Licenciatura', "Licenciatura",
'Maestría', "Maestría",
'Doctorado', "Doctorado",
'Especialidad', "Especialidad",
'Diplomado', "Diplomado",
'Otro', "Otro",
] ];
export const TIPOS_CICLO: Array<TipoCiclo> = [ export const TIPOS_CICLO: Array<TipoCiclo> = [
'Semestre', "Semestre",
'Cuatrimestre', "Cuatrimestre",
'Trimestre', "Trimestre",
'Otro', "Otro",
] ];
export const PLANES_EXISTENTES = [ export const PLANES_EXISTENTES = [
{ {
id: 'plan-2021-sis', id: "plan-2021-sis",
nombre: 'ISC 2021', nombre: "ISC 2021",
estado: 'Aprobado', estado: "Aprobado",
anio: 2021, anio: 2021,
facultadId: 'ing', facultadId: "ing",
carreraId: 'sis', carreraId: "sis",
}, },
{ {
id: 'plan-2020-ind', id: "plan-2020-ind",
nombre: 'I. Industrial 2020', nombre: "I. Industrial 2020",
estado: 'Aprobado', estado: "Aprobado",
anio: 2020, anio: 2020,
facultadId: 'ing', facultadId: "ing",
carreraId: 'ind', carreraId: "ind",
}, },
{ {
id: 'plan-2019-med', id: "plan-2019-med",
nombre: 'Medicina 2019', nombre: "Medicina 2019",
estado: 'Vigente', estado: "Vigente",
anio: 2019, anio: 2019,
facultadId: 'med', facultadId: "med",
carreraId: 'medico', carreraId: "medico",
}, },
] ];
export const ARCHIVOS = [ export const ARCHIVOS = [
{ {
id: 'file-1', id: "file-1",
nombre: 'Sílabo POO 2023.docx', nombre: "Sílabo POO 2023.docx",
tipo: 'docx', tipo: "docx",
tamaño: '245 KB', tamaño: "245 KB",
}, },
{ {
id: 'file-2', id: "file-2",
nombre: 'Guía de prácticas BD.pdf', nombre: "Guía de prácticas BD.pdf",
tipo: 'pdf', tipo: "pdf",
tamaño: '1.2 MB', tamaño: "1.2 MB",
}, },
{ {
id: 'file-3', id: "file-3",
nombre: 'Rúbrica evaluación proyectos.xlsx', nombre: "Rúbrica evaluación proyectos.xlsx",
tipo: 'xlsx', tipo: "xlsx",
tamaño: '89 KB', tamaño: "89 KB",
}, },
{ {
id: 'file-4', id: "file-4",
nombre: 'Banco de reactivos IA.docx', nombre: "Banco de reactivos IA.docx",
tipo: 'docx', tipo: "docx",
tamaño: '567 KB', tamaño: "567 KB",
}, },
{ {
id: 'file-5', id: "file-5",
nombre: 'Asignatural didáctico Web.pdf', nombre: "Material didáctico Web.pdf",
tipo: 'pdf', tipo: "pdf",
tamaño: '3.4 MB', tamaño: "3.4 MB",
}, },
] ];
export const REPOSITORIOS = [ export const REPOSITORIOS = [
{ {
id: 'repo-1', id: "repo-1",
nombre: 'Asignaturales ISC 2024', nombre: "Materiales ISC 2024",
descripcion: 'Documentos de referencia para Ingeniería en Sistemas', descripcion: "Documentos de referencia para Ingeniería en Sistemas",
cantidadArchivos: 45, cantidadArchivos: 45,
}, },
{ {
id: 'repo-2', id: "repo-2",
nombre: 'Lineamientos SEP', nombre: "Lineamientos SEP",
descripcion: 'Documentos oficiales y normativas SEP actualizadas', descripcion: "Documentos oficiales y normativas SEP actualizadas",
cantidadArchivos: 12, cantidadArchivos: 12,
}, },
{ {
id: 'repo-3', id: "repo-3",
nombre: 'Bibliografía Digital', nombre: "Bibliografía Digital",
descripcion: 'Recursos bibliográficos digitalizados', descripcion: "Recursos bibliográficos digitalizados",
cantidadArchivos: 128, cantidadArchivos: 128,
}, },
{ {
id: 'repo-4', id: "repo-4",
nombre: 'Plantillas Institucionales', nombre: "Plantillas Institucionales",
descripcion: 'Formatos y plantillas oficiales ULSA', descripcion: "Formatos y plantillas oficiales ULSA",
cantidadArchivos: 23, cantidadArchivos: 23,
}, },
] ];
export const PLANTILLAS_ANEXO_1 = [ export const PLANTILLAS_ANEXO_1 = [
{ {
id: 'sep-2025', id: "sep-2025",
name: 'Licenciatura RVOE SEP.docx', name: "Licenciatura RVOE SEP.docx",
versions: ['v2025.2 (Vigente)', 'v2025.1', 'v2024.Final'], versions: ["v2025.2 (Vigente)", "v2025.1", "v2024.Final"],
}, },
{ {
id: 'interno-mix', id: "interno-mix",
name: 'Estándar Institucional Mixto.docx', name: "Estándar Institucional Mixto.docx",
versions: ['v2.0', 'v1.5', 'v1.0-beta'], versions: ["v2.0", "v1.5", "v1.0-beta"],
}, },
{ {
id: 'conacyt', id: "conacyt",
name: 'Formato Posgrado CONAHCYT.docx', name: "Formato Posgrado CONAHCYT.docx",
versions: ['v3.0 (2025)', 'v2.8'], versions: ["v3.0 (2025)", "v2.8"],
}, },
] ];
export const PLANTILLAS_ANEXO_2 = [ export const PLANTILLAS_ANEXO_2 = [
{ {
id: 'sep-2017-xlsx', id: "sep-2017-xlsx",
name: 'Licenciatura RVOE 2017.xlsx', name: "Licenciatura RVOE 2017.xlsx",
versions: ['v2017.0', 'v2018.1', 'v2019.2', 'v2020.Final'], versions: ["v2017.0", "v2018.1", "v2019.2", "v2020.Final"],
}, },
{ {
id: 'interno-mix-xlsx', id: "interno-mix-xlsx",
name: 'Estándar Institucional Mixto.xlsx', name: "Estándar Institucional Mixto.xlsx",
versions: ['v1.0', 'v1.5'], versions: ["v1.0", "v1.5"],
}, },
{ {
id: 'conacyt-xlsx', id: "conacyt-xlsx",
name: 'Formato Posgrado CONAHCYT.xlsx', name: "Formato Posgrado CONAHCYT.xlsx",
versions: ['v1.0', 'v2.0'], versions: ["v1.0", "v2.0"],
}, },
] ];

View File

@@ -8,8 +8,8 @@ export function useNuevoPlanWizard() {
tipoOrigen: null, tipoOrigen: null,
datosBasicos: { datosBasicos: {
nombrePlan: '', nombrePlan: '',
facultad: { id: '', nombre: '' }, carreraId: '',
carrera: { id: '', nombre: '' }, facultadId: '',
nivel: '', nivel: '',
tipoCiclo: '', tipoCiclo: '',
numCiclos: undefined, numCiclos: undefined,
@@ -53,8 +53,8 @@ export function useNuevoPlanWizard() {
const canContinueDesdeBasicos = const canContinueDesdeBasicos =
!!wizard.datosBasicos.nombrePlan && !!wizard.datosBasicos.nombrePlan &&
!!wizard.datosBasicos.carrera.id && !!wizard.datosBasicos.carreraId &&
!!wizard.datosBasicos.facultad.id && !!wizard.datosBasicos.facultadId &&
!!wizard.datosBasicos.nivel && !!wizard.datosBasicos.nivel &&
wizard.datosBasicos.numCiclos !== undefined && wizard.datosBasicos.numCiclos !== undefined &&
wizard.datosBasicos.numCiclos > 0 && wizard.datosBasicos.numCiclos > 0 &&

View File

@@ -19,14 +19,8 @@ export type NewPlanWizardState = {
tipoOrigen: TipoOrigen | null tipoOrigen: TipoOrigen | null
datosBasicos: { datosBasicos: {
nombrePlan: string nombrePlan: string
facultad: { carreraId: string
id: string facultadId: string
nombre: string
}
carrera: {
id: string
nombre: string
}
nivel: NivelPlanEstudio | '' nivel: NivelPlanEstudio | ''
tipoCiclo: TipoCiclo | '' tipoCiclo: TipoCiclo | ''
numCiclos: number | undefined numCiclos: number | undefined

View File

@@ -225,7 +225,7 @@ function RouteComponent() {
Mapa Curricular Mapa Curricular
</Tab> </Tab>
<Tab to="/planes/$planId/asignaturas" params={{ planId }}> <Tab to="/planes/$planId/asignaturas" params={{ planId }}>
Asignaturas Materias
</Tab> </Tab>
<Tab to="/planes/$planId/flujo" params={{ planId }}> <Tab to="/planes/$planId/flujo" params={{ planId }}>
Flujo y Estados Flujo y Estados

View File

@@ -0,0 +1,137 @@
import * as Dialog from '@radix-ui/react-dialog';
import { Pencil, X } from 'lucide-react';
export type Materia = {
id: string;
clave: string;
nombre: string;
creditos: number;
hd: number; // Horas Docente
hi: number; // Horas Independientes
tipo: 'Obligatoria' | 'Optativa' | 'Especialidad';
ciclo: number;
linea: string;
estado: string;
};
interface MateriaCardProps {
materia: Materia;
}
export function MateriaCard({ materia }: MateriaCardProps) {
return (
<Dialog.Root>
{/* Trigger: La tarjeta en sí misma */}
<Dialog.Trigger asChild>
<div className="group relative flex flex-col p-2 mb-2 rounded-lg border border-slate-200 bg-white hover:border-emerald-500 hover:shadow-md transition-all cursor-pointer select-none">
{/* Header de la tarjeta */}
<div className="flex justify-between items-start mb-1">
<span className="text-[9px] font-mono font-bold text-slate-400 uppercase">{materia.clave}</span>
<div className="flex gap-1">
<span className="px-1.5 py-0.5 rounded-full bg-emerald-100 text-emerald-700 text-[8px] font-bold uppercase">
{materia.tipo === 'Obligatoria' ? 'OB' : 'OP'}
</span>
</div>
</div>
{/* Nombre */}
<h4 className="text-[11px] font-semibold text-slate-800 leading-tight mb-2 min-h-[2rem]">
{materia.nombre}
</h4>
{/* Footer de la tarjeta (Créditos y Horas) */}
<div className="flex justify-between items-center text-[9px] text-slate-500 border-t pt-1 border-slate-50">
<span>{materia.creditos} cr</span>
<div className="flex gap-1">
<span>HD:{materia.hd}</span>
<span>HI:{materia.hi}</span>
</div>
</div>
{/* Overlay de Hover (Opcional: un iconito de editar) */}
<div className="absolute top-1 right-1 opacity-0 group-hover:opacity-100 transition-opacity">
<Pencil className="w-3 h-3 text-emerald-600" />
</div>
</div>
</Dialog.Trigger>
{/* Modal / Portal */}
<Dialog.Portal>
<Dialog.Overlay className="fixed inset-0 bg-black/40 backdrop-blur-sm z-50 animate-in fade-in" />
<Dialog.Content className="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-full max-w-md bg-white rounded-xl shadow-2xl p-6 z-50 border border-slate-200 animate-in zoom-in-95">
<div className="flex justify-between items-center mb-6">
<Dialog.Title className="text-lg font-bold text-slate-800">Editar Materia</Dialog.Title>
<Dialog.Close className="text-slate-400 hover:text-slate-600 transition-colors">
<X className="w-5 h-5" />
</Dialog.Close>
</div>
<form className="space-y-4">
{/* Clave y Nombre */}
<div className="grid grid-cols-3 gap-4">
<div className="flex flex-col gap-1.5">
<label className="text-xs font-bold text-slate-600 uppercase">Clave</label>
<input
defaultValue={materia.clave}
className="px-3 py-2 rounded-lg border border-slate-300 focus:ring-2 focus:ring-emerald-500 outline-none text-sm font-mono"
/>
</div>
<div className="col-span-2 flex flex-col gap-1.5">
<label className="text-xs font-bold text-slate-600 uppercase">Nombre</label>
<input
defaultValue={materia.nombre}
className="px-3 py-2 rounded-lg border border-slate-300 focus:ring-2 focus:ring-emerald-500 outline-none text-sm"
/>
</div>
</div>
{/* Créditos y Horas */}
<div className="grid grid-cols-3 gap-4">
<div className="flex flex-col gap-1.5">
<label className="text-xs font-bold text-slate-600 uppercase italic">Créditos</label>
<input type="number" defaultValue={materia.creditos} className="px-3 py-2 rounded-lg border border-slate-300 text-sm" />
</div>
<div className="flex flex-col gap-1.5">
<label className="text-xs font-bold text-slate-600 uppercase italic">HD (Hrs Docente)</label>
<input type="number" defaultValue={materia.hd} className="px-3 py-2 rounded-lg border border-slate-300 text-sm" />
</div>
<div className="flex flex-col gap-1.5">
<label className="text-xs font-bold text-slate-600 uppercase italic">HI (Hrs Indep.)</label>
<input type="number" defaultValue={materia.hi} className="px-3 py-2 rounded-lg border border-slate-300 text-sm" />
</div>
</div>
{/* Ciclo y Línea */}
<div className="grid grid-cols-2 gap-4">
<div className="flex flex-col gap-1.5">
<label className="text-xs font-bold text-slate-600 uppercase">Ciclo</label>
<select className="px-3 py-2 rounded-lg border border-slate-300 text-sm bg-white">
<option>Ciclo {materia.ciclo}</option>
</select>
</div>
<div className="flex flex-col gap-1.5">
<label className="text-xs font-bold text-slate-600 uppercase">Línea Curricular</label>
<select className="px-3 py-2 rounded-lg border border-slate-300 text-sm bg-white">
<option>{materia.linea}</option>
</select>
</div>
</div>
{/* Botones de acción */}
<div className="flex justify-end gap-3 pt-6">
<Dialog.Close className="px-4 py-2 rounded-lg text-sm font-semibold text-slate-600 hover:bg-slate-100 transition-colors">
Cancelar
</Dialog.Close>
<button
type="button"
className="px-6 py-2 rounded-lg text-sm font-semibold bg-emerald-700 text-white hover:bg-emerald-800 transition-colors shadow-sm"
>
Guardar
</button>
</div>
</form>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
);
}

View File

@@ -10,7 +10,7 @@ import {
} from 'lucide-react' } from 'lucide-react'
import { useState, useMemo } from 'react' import { useState, useMemo } from 'react'
import type { Asignatura, AsignaturaStatus, TipoAsignatura } from '@/types/plan' import type { Materia } from '@/types/plan'
import { Badge } from '@/components/ui/badge' import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
@@ -33,27 +33,20 @@ import {
import { usePlanAsignaturas, usePlanLineas } from '@/data' import { usePlanAsignaturas, usePlanLineas } from '@/data'
// --- Configuración de Estilos --- // --- Configuración de Estilos ---
const statusConfig: Record< const statusConfig: Record<string, { label: string; className: string }> = {
AsignaturaStatus,
{ label: string; className: string }
> = {
borrador: { label: 'Borrador', className: 'bg-slate-100 text-slate-600' }, borrador: { label: 'Borrador', className: 'bg-slate-100 text-slate-600' },
revisada: { label: 'Revisada', className: 'bg-amber-100 text-amber-700' }, revisada: { label: 'Revisada', className: 'bg-amber-100 text-amber-700' },
aprobada: { label: 'Aprobada', className: 'bg-emerald-100 text-emerald-700' }, aprobada: { label: 'Aprobada', className: 'bg-emerald-100 text-emerald-700' },
} }
const tipoConfig: Record<TipoAsignatura, { label: string; className: string }> = const tipoConfig: Record<string, { label: string; className: string }> = {
{ obligatoria: { label: 'Obligatoria', className: 'bg-blue-100 text-blue-700' },
obligatoria: { optativa: { label: 'Optativa', className: 'bg-purple-100 text-purple-700' },
label: 'Obligatoria', troncal: { label: 'Troncal', className: 'bg-slate-100 text-slate-700' },
className: 'bg-blue-100 text-blue-700', }
},
optativa: { label: 'Optativa', className: 'bg-purple-100 text-purple-700' },
troncal: { label: 'Troncal', className: 'bg-slate-100 text-slate-700' },
}
// --- Mapeadores de API --- // --- Mapeadores de API ---
const mapAsignaturas = (asigApi: Array<any> = []): Array<Asignatura> => { const mapAsignaturas = (asigApi: Array<any> = []): Array<Materia> => {
return asigApi.map((asig) => ({ return asigApi.map((asig) => ({
id: asig.id, id: asig.id,
clave: asig.codigo, clave: asig.codigo,
@@ -66,17 +59,14 @@ const mapAsignaturas = (asigApi: Array<any> = []): Array<Asignatura> => {
estado: 'borrador', // O el campo que venga de tu API estado: 'borrador', // O el campo que venga de tu API
hd: Math.floor((asig.horas_semana ?? 0) / 2), hd: Math.floor((asig.horas_semana ?? 0) / 2),
hi: Math.ceil((asig.horas_semana ?? 0) / 2), hi: Math.ceil((asig.horas_semana ?? 0) / 2),
prerrequisitos: Array.isArray(asig.prerrequisitos)
? asig.prerrequisitos
: [],
})) }))
} }
export const Route = createFileRoute('/planes/$planId/_detalle/asignaturas/')({ export const Route = createFileRoute('/planes/$planId/_detalle/asignaturas/')({
component: AsignaturasPage, component: MateriasPage,
}) })
function AsignaturasPage() { function MateriasPage() {
const { planId } = Route.useParams() const { planId } = Route.useParams()
const navigate = useNavigate() const navigate = useNavigate()
@@ -92,13 +82,13 @@ function AsignaturasPage() {
const [filterLinea, setFilterLinea] = useState<string>('all') const [filterLinea, setFilterLinea] = useState<string>('all')
// 3. Procesamiento de datos // 3. Procesamiento de datos
const asignaturas = useMemo( const materias = useMemo(
() => mapAsignaturas(asignaturasApi), () => mapAsignaturas(asignaturasApi),
[asignaturasApi], [asignaturasApi],
) )
const lineas = useMemo(() => lineasApi || [], [lineasApi]) const lineas = useMemo(() => lineasApi || [], [lineasApi])
const filteredAsignaturas = asignaturas.filter((m) => { const filteredMaterias = materias.filter((m) => {
const matchesSearch = const matchesSearch =
m.nombre.toLowerCase().includes(searchTerm.toLowerCase()) || m.nombre.toLowerCase().includes(searchTerm.toLowerCase()) ||
m.clave.toLowerCase().includes(searchTerm.toLowerCase()) m.clave.toLowerCase().includes(searchTerm.toLowerCase())
@@ -129,11 +119,11 @@ function AsignaturasPage() {
<div className="flex flex-wrap items-center justify-between gap-4"> <div className="flex flex-wrap items-center justify-between gap-4">
<div> <div>
<h2 className="text-foreground text-xl font-bold"> <h2 className="text-foreground text-xl font-bold">
Asignaturas del Plan Materias del Plan
</h2> </h2>
<p className="text-muted-foreground mt-1 text-sm"> <p className="text-muted-foreground mt-1 text-sm">
{asignaturas.length} asignaturas en total {' '} {materias.length} materias en total {filteredMaterias.length}{' '}
{filteredAsignaturas.length} filtradas filtradas
</p> </p>
</div> </div>
@@ -152,7 +142,7 @@ function AsignaturasPage() {
}} }}
className="ring-offset-background bg-primary text-primary-foreground hover:bg-primary/90 inline-flex h-11 items-center justify-center gap-2 rounded-md px-8 text-sm font-medium shadow-md transition-colors" className="ring-offset-background bg-primary text-primary-foreground hover:bg-primary/90 inline-flex h-11 items-center justify-center gap-2 rounded-md px-8 text-sm font-medium shadow-md transition-colors"
> >
<Plus className="mr-2 h-4 w-4" /> Nueva Asignatura <Plus className="mr-2 h-4 w-4" /> Nueva Materia
</Button> </Button>
</div> </div>
</div> </div>
@@ -227,12 +217,12 @@ function AsignaturasPage() {
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{filteredAsignaturas.length === 0 ? ( {filteredMaterias.length === 0 ? (
<TableRow> <TableRow>
<TableCell colSpan={8} className="h-40 text-center"> <TableCell colSpan={8} className="h-40 text-center">
<div className="text-muted-foreground flex flex-col items-center justify-center"> <div className="text-muted-foreground flex flex-col items-center justify-center">
<BookOpen className="mb-2 h-10 w-10 opacity-20" /> <BookOpen className="mb-2 h-10 w-10 opacity-20" />
<p className="font-medium">No se encontraron asignaturas</p> <p className="font-medium">No se encontraron materias</p>
<p className="text-xs"> <p className="text-xs">
Intenta cambiar los filtros de búsqueda Intenta cambiar los filtros de búsqueda
</p> </p>
@@ -240,59 +230,59 @@ function AsignaturasPage() {
</TableCell> </TableCell>
</TableRow> </TableRow>
) : ( ) : (
filteredAsignaturas.map((asignatura) => ( filteredMaterias.map((materia) => (
<TableRow <TableRow
key={asignatura.id} key={materia.id}
className="group cursor-pointer transition-colors hover:bg-slate-50/80" className="group cursor-pointer transition-colors hover:bg-slate-50/80"
onClick={() => onClick={() =>
navigate({ navigate({
to: '/planes/$planId/asignaturas/$asignaturaId', to: '/planes/$planId/asignaturas/$asignaturaId',
params: { params: {
planId, planId,
asignaturaId: asignatura.id, // 👈 puede ser índice, consecutivo o slug asignaturaId: materia.id, // 👈 puede ser índice, consecutivo o slug
}, },
state: { state: {
realId: asignatura.id, // 👈 ID largo oculto realId: materia.id, // 👈 ID largo oculto
asignaturaId: asignatura.id, asignaturaId: materia.id,
} as any, } as any,
}) })
} }
> >
<TableCell className="font-mono text-xs font-bold text-slate-400"> <TableCell className="font-mono text-xs font-bold text-slate-400">
{asignatura.clave} {materia.clave}
</TableCell> </TableCell>
<TableCell className="font-semibold text-slate-700"> <TableCell className="font-semibold text-slate-700">
{asignatura.nombre} {materia.nombre}
</TableCell> </TableCell>
<TableCell className="text-center font-medium"> <TableCell className="text-center font-medium">
{asignatura.creditos} {materia.creditos}
</TableCell> </TableCell>
<TableCell className="text-center"> <TableCell className="text-center">
{asignatura.ciclo ? ( {materia.ciclo ? (
<Badge variant="outline" className="font-normal"> <Badge variant="outline" className="font-normal">
Ciclo {asignatura.ciclo} Ciclo {materia.ciclo}
</Badge> </Badge>
) : ( ) : (
<span className="text-slate-300"></span> <span className="text-slate-300"></span>
)} )}
</TableCell> </TableCell>
<TableCell className="text-sm text-slate-600"> <TableCell className="text-sm text-slate-600">
{getLineaNombre(asignatura.lineaCurricularId)} {getLineaNombre(materia.lineaCurricularId)}
</TableCell> </TableCell>
<TableCell> <TableCell>
<Badge <Badge
variant="outline" variant="outline"
className={`capitalize shadow-sm ${tipoConfig[asignatura.tipo].className}`} className={`capitalize shadow-sm ${tipoConfig[materia.tipo]?.className}`}
> >
{tipoConfig[asignatura.tipo].label} {tipoConfig[materia.tipo]?.label}
</Badge> </Badge>
</TableCell> </TableCell>
<TableCell> <TableCell>
<Badge <Badge
variant="outline" variant="outline"
className={`capitalize shadow-sm ${statusConfig[asignatura.estado].className}`} className={`capitalize shadow-sm ${statusConfig[materia.estado]?.className}`}
> >
{statusConfig[asignatura.estado].label} {statusConfig[materia.estado]?.label}
</Badge> </Badge>
</TableCell> </TableCell>
<TableCell> <TableCell>

View File

@@ -87,33 +87,27 @@ function RouteComponent() {
// 1. Transformar datos de la API para el menú de selección // 1. Transformar datos de la API para el menú de selección
const availableFields = useMemo(() => { const availableFields = useMemo(() => {
if (!data?.estructuras_plan?.definicion?.properties) return [] if (!data?.datos) return []
return Object.entries(data.estructuras_plan.definicion.properties).map( return Object.entries(data.datos).map(([key, value]) => ({
([key, value]) => ({ key,
key, label: formatLabel(key),
label: value.title, value: String(value || ''),
value: String(value.description || ''), }))
}),
)
}, [data]) }, [data])
// 2. Manejar el estado inicial si viene de "Datos Generales" // 2. Manejar el estado inicial si viene de "Datos Generales"
useEffect(() => { useEffect(() => {
const state = routerState.location.state as any const state = routerState.location.state as any
if (!state?.campo_edit || availableFields.length === 0) return if (state?.prefill && availableFields.length > 0) {
// Intentamos encontrar qué campo es por su valor o si mandaste el fieldKey
const field = availableFields.find( const field = availableFields.find(
(f) => (f) => f.value === state.prefill || f.key === state.fieldKey,
f.value === state.campo_edit.label || f.key === state.campo_edit.clave, )
) if (field && !selectedFields.find((sf) => sf.key === field.key)) {
setSelectedFields([field])
if (field && !selectedFields.some((sf) => sf.key === field.key)) { }
setSelectedFields([field]) setInput(`Mejora este campo: ${field?.label} `)
} }
setInput((prev) =>
injectFieldsIntoInput(prev || 'Mejora este campo:', field ? [field] : []),
)
}, [availableFields]) }, [availableFields])
// 3. Lógica para el disparador ":" // 3. Lógica para el disparador ":"
@@ -133,7 +127,7 @@ function RouteComponent() {
) => { ) => {
const baseText = input.replace(/\[[^\]]+]/g, '').trim() const baseText = input.replace(/\[[^\]]+]/g, '').trim()
const tags = fields.map((f) => `${f.label}`).join(' ') const tags = fields.map((f) => `[${f.label}]`).join(' ')
return `${baseText} ${tags}`.trim() return `${baseText} ${tags}`.trim()
} }

View File

@@ -81,7 +81,6 @@ function DatosGeneralesPage() {
const rawValue = valores[key] const rawValue = valores[key]
return { return {
clave: key,
id: (index + 1).toString(), id: (index + 1).toString(),
label: schema?.title || formatLabel(key), label: schema?.title || formatLabel(key),
helperText: schema?.description || '', helperText: schema?.description || '',
@@ -132,16 +131,14 @@ function DatosGeneralesPage() {
// toast.success('Cambios guardados localmente') // toast.success('Cambios guardados localmente')
} }
const handleIARequest = (clave: string) => { const handleIARequest = (descripcion: string) => {
console.log(clave)
navigate({ navigate({
to: '/planes/$planId/iaplan', to: '/planes/$planId/iaplan',
params: { params: {
planId: planId, // o dinámico planId: planId, // o dinámico
}, },
state: { state: {
campo_edit: clave, prefill: descripcion,
} as any, } as any,
}) })
} }
@@ -157,9 +154,8 @@ function DatosGeneralesPage() {
</div> </div>
<div className="grid grid-cols-1 gap-6 lg:grid-cols-2"> <div className="grid grid-cols-1 gap-6 lg:grid-cols-2">
{campos.map((campo, key) => { {campos.map((campo) => {
const isEditing = editingId === campo.id const isEditing = editingId === campo.id
console.log(campo)
return ( return (
<div <div
@@ -198,7 +194,7 @@ function DatosGeneralesPage() {
variant="ghost" variant="ghost"
size="icon" size="icon"
className="h-8 w-8 text-teal-600" className="h-8 w-8 text-teal-600"
onClick={() => handleIARequest(campo)} onClick={() => handleIARequest(campo.value)}
> >
<Sparkles size={14} /> <Sparkles size={14} />
</Button> </Button>

View File

@@ -9,7 +9,7 @@ import {
} from 'lucide-react' } from 'lucide-react'
import { useMemo, useState, useEffect } from 'react' import { useMemo, useState, useEffect } from 'react'
import type { Asignatura, LineaCurricular } from '@/types/plan' import type { Materia, LineaCurricular } from '@/types/plan'
import { Badge } from '@/components/ui/badge' import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
@@ -47,9 +47,7 @@ const mapLineasToLineaCurricular = (
})) }))
} }
const mapAsignaturasToAsignaturas = ( const mapAsignaturasToMaterias = (asigApi: Array<any> = []): Array<Materia> => {
asigApi: Array<any> = [],
): Array<Asignatura> => {
return asigApi.map((asig) => ({ return asigApi.map((asig) => ({
id: asig.id, id: asig.id,
clave: asig.codigo, clave: asig.codigo,
@@ -106,13 +104,13 @@ function StatItem({
) )
} }
function AsignaturaCardItem({ function MateriaCardItem({
asignatura, materia,
onDragStart, onDragStart,
isDragging, isDragging,
onClick, onClick,
}: { }: {
asignatura: Asignatura materia: Materia
onDragStart: (e: React.DragEvent, id: string) => void onDragStart: (e: React.DragEvent, id: string) => void
isDragging: boolean isDragging: boolean
onClick: () => void onClick: () => void
@@ -120,7 +118,7 @@ function AsignaturaCardItem({
return ( return (
<button <button
draggable draggable
onDragStart={(e) => onDragStart(e, asignatura.id)} onDragStart={(e) => onDragStart(e, materia.id)}
onClick={onClick} onClick={onClick}
className={`group cursor-grab rounded-lg border bg-white p-3 shadow-sm transition-all active:cursor-grabbing ${ className={`group cursor-grab rounded-lg border bg-white p-3 shadow-sm transition-all active:cursor-grabbing ${
isDragging isDragging
@@ -130,21 +128,21 @@ function AsignaturaCardItem({
> >
<div className="mb-1 flex items-start justify-between"> <div className="mb-1 flex items-start justify-between">
<span className="font-mono text-[10px] font-bold text-slate-400"> <span className="font-mono text-[10px] font-bold text-slate-400">
{asignatura.clave} {materia.clave}
</span> </span>
<Badge <Badge
variant="outline" variant="outline"
className={`px-1 py-0 text-[9px] uppercase ${statusBadge[asignatura.estado] || ''}`} className={`px-1 py-0 text-[9px] uppercase ${statusBadge[materia.estado] || ''}`}
> >
{asignatura.estado} {materia.estado}
</Badge> </Badge>
</div> </div>
<p className="mb-1 text-xs leading-tight font-bold text-slate-700"> <p className="mb-1 text-xs leading-tight font-bold text-slate-700">
{asignatura.nombre} {materia.nombre}
</p> </p>
<div className="mt-2 flex items-center justify-between"> <div className="mt-2 flex items-center justify-between">
<span className="text-[10px] text-slate-500"> <span className="text-[10px] text-slate-500">
{asignatura.creditos} CR HD:{asignatura.hd} HI:{asignatura.hi} {materia.creditos} CR HD:{materia.hd} HI:{materia.hi}
</span> </span>
<GripVertical <GripVertical
size={12} size={12}
@@ -168,14 +166,11 @@ function MapaCurricularPage() {
const { data: lineasApi, isLoading: loadingLineas } = usePlanLineas(planId) const { data: lineasApi, isLoading: loadingLineas } = usePlanLineas(planId)
// 2. Estado Local (Para interactividad) // 2. Estado Local (Para interactividad)
const [asignaturas, setAsignaturas] = useState<Array<Asignatura>>([]) const [materias, setMaterias] = useState<Array<Materia>>([])
const [lineas, setLineas] = useState<Array<LineaCurricular>>([]) const [lineas, setLineas] = useState<Array<LineaCurricular>>([])
const [draggedAsignatura, setDraggedAsignatura] = useState<string | null>( const [draggedMateria, setDraggedMateria] = useState<string | null>(null)
null,
)
const [isEditModalOpen, setIsEditModalOpen] = useState(false) const [isEditModalOpen, setIsEditModalOpen] = useState(false)
const [selectedAsignatura, setSelectedAsignatura] = const [selectedMateria, setSelectedMateria] = useState<Materia | null>(null)
useState<Asignatura | null>(null)
const [hasAreaComun, setHasAreaComun] = useState(false) const [hasAreaComun, setHasAreaComun] = useState(false)
const [nombreNuevaLinea, setNombreNuevaLinea] = useState('') // Para el input de nombre personalizado const [nombreNuevaLinea, setNombreNuevaLinea] = useState('') // Para el input de nombre personalizado
@@ -241,8 +236,7 @@ function MapaCurricularPage() {
// 3. Sincronizar API -> Estado Local // 3. Sincronizar API -> Estado Local
useEffect(() => { useEffect(() => {
if (asignaturasApi) if (asignaturasApi) setMaterias(mapAsignaturasToMaterias(asignaturasApi))
setAsignaturas(mapAsignaturasToAsignaturas(asignaturasApi))
}, [asignaturasApi]) }, [asignaturasApi])
useEffect(() => { useEffect(() => {
@@ -253,23 +247,23 @@ function MapaCurricularPage() {
const ciclosArray = Array.from({ length: ciclosTotales }, (_, i) => i + 1) const ciclosArray = Array.from({ length: ciclosTotales }, (_, i) => i + 1)
// Nuevo estado para controlar los datos temporales del modal de edición // Nuevo estado para controlar los datos temporales del modal de edición
const [editingData, setEditingData] = useState<Asignatura | null>(null) const [editingData, setEditingData] = useState<Materia | null>(null)
// 1. FUNCION DE GUARDAR MODAL // 1. FUNCION DE GUARDAR MODAL
const handleSaveChanges = () => { const handleSaveChanges = () => {
if (!editingData) return if (!editingData) return
console.log(asignaturas) console.log(materias)
setAsignaturas((prev) => setMaterias((prev) =>
prev.map((m) => (m.id === editingData.id ? { ...editingData } : m)), prev.map((m) => (m.id === editingData.id ? { ...editingData } : m)),
) )
setIsEditModalOpen(false) setIsEditModalOpen(false)
} }
// 2. MODIFICACIÓN: Zona de soltado siempre visible // 2. MODIFICACIÓN: Zona de soltado siempre visible
// Cambiamos la condición: Mostramos la sección si hay asignaturas sin asignar // Cambiamos la condición: Mostramos la sección si hay materias sin asignar
// O si simplemente queremos tener el "depósito" disponible. // O si simplemente queremos tener el "depósito" disponible.
const unassignedAsignaturas = asignaturas.filter((m) => m.ciclo === null) const unassignedMaterias = materias.filter((m) => m.ciclo === null)
// --- Lógica de Gestión --- // --- Lógica de Gestión ---
const agregarLinea = (nombre: string) => { const agregarLinea = (nombre: string) => {
@@ -278,7 +272,7 @@ function MapaCurricularPage() {
} }
const borrarLinea = (id: string) => { const borrarLinea = (id: string) => {
setAsignaturas((prev) => setMaterias((prev) =>
prev.map((m) => prev.map((m) =>
m.lineaCurricularId === id m.lineaCurricularId === id
? { ...m, ciclo: null, lineaCurricularId: null } ? { ...m, ciclo: null, lineaCurricularId: null }
@@ -290,7 +284,7 @@ function MapaCurricularPage() {
// --- Selectores/Cálculos --- // --- Selectores/Cálculos ---
const getTotalesCiclo = (ciclo: number) => { const getTotalesCiclo = (ciclo: number) => {
return asignaturas return materias
.filter((m) => m.ciclo === ciclo) .filter((m) => m.ciclo === ciclo)
.reduce( .reduce(
(acc, m) => ({ (acc, m) => ({
@@ -303,7 +297,7 @@ function MapaCurricularPage() {
} }
const getSubtotalLinea = (lineaId: string) => { const getSubtotalLinea = (lineaId: string) => {
return asignaturas return materias
.filter((m) => m.lineaCurricularId === lineaId && m.ciclo !== null) .filter((m) => m.lineaCurricularId === lineaId && m.ciclo !== null)
.reduce( .reduce(
(acc, m) => ({ (acc, m) => ({
@@ -316,7 +310,7 @@ function MapaCurricularPage() {
} }
const handleDragStart = (e: React.DragEvent, id: string) => { const handleDragStart = (e: React.DragEvent, id: string) => {
setDraggedAsignatura(id) setDraggedMateria(id)
e.dataTransfer.effectAllowed = 'move' e.dataTransfer.effectAllowed = 'move'
} }
const handleDragOver = (e: React.DragEvent) => e.preventDefault() const handleDragOver = (e: React.DragEvent) => e.preventDefault()
@@ -326,21 +320,21 @@ function MapaCurricularPage() {
lineaId: string | null, lineaId: string | null,
) => { ) => {
e.preventDefault() e.preventDefault()
if (draggedAsignatura) { if (draggedMateria) {
setAsignaturas((prev) => setMaterias((prev) =>
prev.map((m) => prev.map((m) =>
m.id === draggedAsignatura m.id === draggedMateria
? { ...m, ciclo, lineaCurricularId: lineaId } ? { ...m, ciclo, lineaCurricularId: lineaId }
: m, : m,
), ),
) )
setDraggedAsignatura(null) setDraggedMateria(null)
} }
} }
const stats = useMemo( const stats = useMemo(
() => () =>
asignaturas.reduce( materias.reduce(
(acc, m) => { (acc, m) => {
if (m.ciclo !== null) { if (m.ciclo !== null) {
acc.cr += m.creditos || 0 acc.cr += m.creditos || 0
@@ -351,7 +345,7 @@ function MapaCurricularPage() {
}, },
{ cr: 0, hd: 0, hi: 0 }, { cr: 0, hd: 0, hi: 0 },
), ),
[asignaturas], [materias],
) )
if (loadingAsig || loadingLineas) if (loadingAsig || loadingLineas)
@@ -364,14 +358,14 @@ function MapaCurricularPage() {
<div> <div>
<h2 className="text-xl font-bold">Mapa Curricular</h2> <h2 className="text-xl font-bold">Mapa Curricular</h2>
<p className="text-sm text-slate-500"> <p className="text-sm text-slate-500">
Organiza las asignaturas de la petición por línea y ciclo Organiza las materias de la petición por línea y ciclo
</p> </p>
</div> </div>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
{asignaturas.filter((m) => !m.ciclo).length > 0 && ( {materias.filter((m) => !m.ciclo).length > 0 && (
<Badge className="border-amber-100 bg-amber-50 text-amber-600 hover:bg-amber-50"> <Badge className="border-amber-100 bg-amber-50 text-amber-600 hover:bg-amber-50">
<AlertTriangle size={14} className="mr-1" />{' '} <AlertTriangle size={14} className="mr-1" />{' '}
{asignaturas.filter((m) => !m.ciclo).length} sin asignar {materias.filter((m) => !m.ciclo).length} sin asignar
</Badge> </Badge>
)} )}
<DropdownMenu> <DropdownMenu>
@@ -480,16 +474,16 @@ function MapaCurricularPage() {
onDrop={(e) => handleDrop(e, ciclo, linea.id)} onDrop={(e) => handleDrop(e, ciclo, linea.id)}
className="min-h-[140px] space-y-2 rounded-xl border-2 border-dashed border-slate-100 bg-slate-50/20 p-2" className="min-h-[140px] space-y-2 rounded-xl border-2 border-dashed border-slate-100 bg-slate-50/20 p-2"
> >
{asignaturas {materias
.filter( .filter(
(m) => (m) =>
m.ciclo === ciclo && m.lineaCurricularId === linea.id, m.ciclo === ciclo && m.lineaCurricularId === linea.id,
) )
.map((m) => ( .map((m) => (
<AsignaturaCardItem <MateriaCardItem
key={m.id} key={m.id}
asignatura={m} materia={m}
isDragging={draggedAsignatura === m.id} isDragging={draggedMateria === m.id}
onDragStart={handleDragStart} onDragStart={handleDragStart}
onClick={() => { onClick={() => {
setEditingData(m) setEditingData(m)
@@ -540,35 +534,35 @@ function MapaCurricularPage() {
</div> </div>
</div> </div>
{/* Asignaturas Sin Asignar */} {/* Materias Sin Asignar */}
{/* SECCIÓN DE MATERIAS SIN ASIGNAR (Mejorada para estar siempre disponible) */} {/* SECCIÓN DE MATERIAS SIN ASIGNAR (Mejorada para estar siempre disponible) */}
<div className="mt-10 rounded-2xl border border-slate-200 bg-slate-50 p-6"> <div className="mt-10 rounded-2xl border border-slate-200 bg-slate-50 p-6">
<div className="mb-4 flex items-center justify-between"> <div className="mb-4 flex items-center justify-between">
<div className="flex items-center gap-2 text-slate-600"> <div className="flex items-center gap-2 text-slate-600">
<h3 className="text-sm font-bold tracking-wider uppercase"> <h3 className="text-sm font-bold tracking-wider uppercase">
Bandeja de Entrada / Asignaturas sin asignar Bandeja de Entrada / Materias sin asignar
</h3> </h3>
<Badge variant="secondary">{unassignedAsignaturas.length}</Badge> <Badge variant="secondary">{unassignedMaterias.length}</Badge>
</div> </div>
<p className="text-xs text-slate-400"> <p className="text-xs text-slate-400">
Arrastra una asignatura aquí para quitarla del mapa Arrastra una materia aquí para quitarla del mapa
</p> </p>
</div> </div>
<div <div
className={`flex min-h-[120px] flex-wrap gap-4 rounded-xl border-2 border-dashed p-4 transition-colors ${ className={`flex min-h-[120px] flex-wrap gap-4 rounded-xl border-2 border-dashed p-4 transition-colors ${
draggedAsignatura draggedMateria
? 'border-teal-300 bg-teal-50/50' ? 'border-teal-300 bg-teal-50/50'
: 'border-slate-200 bg-white/50' : 'border-slate-200 bg-white/50'
}`} }`}
onDragOver={handleDragOver} onDragOver={handleDragOver}
onDrop={(e) => handleDrop(e, null, null)} // Limpia ciclo y línea onDrop={(e) => handleDrop(e, null, null)} // Limpia ciclo y línea
> >
{unassignedAsignaturas.map((m) => ( {unassignedMaterias.map((m) => (
<div key={m.id} className="w-[200px]"> <div key={m.id} className="w-[200px]">
<AsignaturaCardItem <MateriaCardItem
asignatura={m} materia={m}
isDragging={draggedAsignatura === m.id} isDragging={draggedMateria === m.id}
onDragStart={handleDragStart} onDragStart={handleDragStart}
onClick={() => { onClick={() => {
setEditingData(m) // Cargamos los datos en el estado de edición setEditingData(m) // Cargamos los datos en el estado de edición
@@ -577,9 +571,9 @@ function MapaCurricularPage() {
/> />
</div> </div>
))} ))}
{unassignedAsignaturas.length === 0 && ( {unassignedMaterias.length === 0 && (
<div className="flex w-full items-center justify-center text-sm text-slate-400"> <div className="flex w-full items-center justify-center text-sm text-slate-400">
No hay asignaturas pendientes. Arrastra una asignatura aquí para No hay materias pendientes. Arrastra una materia aquí para
desasignarla. desasignarla.
</div> </div>
)} )}
@@ -591,7 +585,7 @@ function MapaCurricularPage() {
<DialogContent className="sm:max-w-[550px]"> <DialogContent className="sm:max-w-[550px]">
<DialogHeader> <DialogHeader>
<DialogTitle className="font-bold text-slate-700"> <DialogTitle className="font-bold text-slate-700">
Editar Asignatura Editar Materia
</DialogTitle> </DialogTitle>
</DialogHeader> </DialogHeader>
@@ -741,10 +735,10 @@ function MapaCurricularPage() {
</label> </label>
<Select> <Select>
<SelectTrigger> <SelectTrigger>
<SelectValue placeholder="Seleccionar asignatura..." /> <SelectValue placeholder="Seleccionar materia..." />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{asignaturas.map((m) => ( {materias.map((m) => (
<SelectItem key={m.id} value={m.clave}> <SelectItem key={m.id} value={m.clave}>
{m.nombre} {m.nombre}
</SelectItem> </SelectItem>

View File

@@ -1,6 +1,6 @@
import { createFileRoute, notFound } from '@tanstack/react-router' import { createFileRoute, notFound } from '@tanstack/react-router'
import AsignaturaDetailPage from '@/components/asignaturas/detalle/AsignaturaDetailPage' import MateriaDetailPage from '@/components/asignaturas/detalle/MateriaDetailPage'
import { NotFoundPage } from '@/components/ui/NotFoundPage' import { NotFoundPage } from '@/components/ui/NotFoundPage'
import { subjects_get } from '@/data/api/subjects.api' import { subjects_get } from '@/data/api/subjects.api'
import { qk } from '@/data/query/keys' import { qk } from '@/data/query/keys'
@@ -38,7 +38,7 @@ function RouteComponent() {
return ( return (
<div> <div>
<AsignaturaDetailPage></AsignaturaDetailPage> <MateriaDetailPage></MateriaDetailPage>
</div> </div>
) )
} }

View File

@@ -1,119 +0,0 @@
export type AsignaturaTab =
| 'datos-generales'
| 'contenido-tematico'
| 'bibliografia'
| 'ia-asignatura'
| 'documento-sep'
| 'historial'
export interface Asignatura {
id: string
nombre: string
clave: string
creditos?: number
lineaCurricular?: string
ciclo?: string
planId: string
planNombre: string
carrera: string
facultad: string
estructuraId: string
}
export interface CampoEstructura {
id: string
nombre: string
tipo: 'texto' | 'texto_largo' | 'lista' | 'numero'
obligatorio: boolean
descripcion?: string
placeholder?: string
}
export interface AsignaturaStructure {
id: string
nombre: string
campos: Array<CampoEstructura>
}
export interface Tema {
id: string
nombre: string
descripcion?: string
horasEstimadas?: number
}
export interface UnidadTematica {
id: string
nombre: string
numero: number
temas: Array<Tema>
}
export interface BibliografiaEntry {
id: string
tipo: 'BASICA' | 'COMPLEMENTARIA'
cita: string
fuenteBibliotecaId?: string
fuenteBiblioteca?: LibraryResource
}
export interface LibraryResource {
id: string
titulo: string
autor: string
editorial?: string
anio?: number
isbn?: string
tipo: 'libro' | 'articulo' | 'revista' | 'recurso_digital'
disponible: boolean
}
export interface IAMessage {
id: string
role: 'user' | 'assistant'
content: string
timestamp: Date
campoAfectado?: string
sugerencia?: IASugerencia
}
export interface IASugerencia {
campoId: string
campoNombre: string
valorActual: string
valorSugerido: string
aceptada?: boolean
}
export interface CambioAsignatura {
id: string
tipo: 'datos' | 'contenido' | 'bibliografia' | 'ia' | 'documento'
descripcion: string
usuario: string
fecha: Date
detalles?: Record<string, any>
}
export interface DocumentoAsignatura {
id: string
asignaturaId: string
version: number
fechaGeneracion: Date
url?: string
estado: 'generando' | 'listo' | 'error'
}
export interface AsignaturaDetailState {
asignatura: Asignatura | null
estructura: AsignaturaStructure | null
datosGenerales: Record<string, any>
contenidoTematico: Array<UnidadTematica>
bibliografia: Array<BibliografiaEntry>
iaMessages: Array<IAMessage>
documentoSep: DocumentoAsignatura | null
historial: Array<CambioAsignatura>
activeTab: AsignaturaTab
isSaving: boolean
isLoading: boolean
errorMessage: string | null
}

119
src/types/materia.ts Normal file
View File

@@ -0,0 +1,119 @@
export type MateriaTab =
| 'datos-generales'
| 'contenido-tematico'
| 'bibliografia'
| 'ia-materia'
| 'documento-sep'
| 'historial';
export interface Materia {
id: string;
nombre: string;
clave: string;
creditos?: number;
lineaCurricular?: string;
ciclo?: string;
planId: string;
planNombre: string;
carrera: string;
facultad: string;
estructuraId: string;
}
export interface CampoEstructura {
id: string;
nombre: string;
tipo: 'texto' | 'texto_largo' | 'lista' | 'numero';
obligatorio: boolean;
descripcion?: string;
placeholder?: string;
}
export interface MateriaStructure {
id: string;
nombre: string;
campos: CampoEstructura[];
}
export interface Tema {
id: string;
nombre: string;
descripcion?: string;
horasEstimadas?: number;
}
export interface UnidadTematica {
id: string;
nombre: string;
numero: number;
temas: Tema[];
}
export interface BibliografiaEntry {
id: string;
tipo: 'BASICA' | 'COMPLEMENTARIA';
cita: string;
fuenteBibliotecaId?: string;
fuenteBiblioteca?: LibraryResource;
}
export interface LibraryResource {
id: string;
titulo: string;
autor: string;
editorial?: string;
anio?: number;
isbn?: string;
tipo: 'libro' | 'articulo' | 'revista' | 'recurso_digital';
disponible: boolean;
}
export interface IAMessage {
id: string;
role: 'user' | 'assistant';
content: string;
timestamp: Date;
campoAfectado?: string;
sugerencia?: IASugerencia;
}
export interface IASugerencia {
campoId: string;
campoNombre: string;
valorActual: string;
valorSugerido: string;
aceptada?: boolean;
}
export interface CambioMateria {
id: string;
tipo: 'datos' | 'contenido' | 'bibliografia' | 'ia' | 'documento';
descripcion: string;
usuario: string;
fecha: Date;
detalles?: Record<string, any>;
}
export interface DocumentoMateria {
id: string;
materiaId: string;
version: number;
fechaGeneracion: Date;
url?: string;
estado: 'generando' | 'listo' | 'error';
}
export interface MateriaDetailState {
materia: Materia | null;
estructura: MateriaStructure | null;
datosGenerales: Record<string, any>;
contenidoTematico: UnidadTematica[];
bibliografia: BibliografiaEntry[];
iaMessages: IAMessage[];
documentoSep: DocumentoMateria | null;
historial: CambioMateria[];
activeTab: MateriaTab;
isSaving: boolean;
isLoading: boolean;
errorMessage: string | null;
}

View File

@@ -12,9 +12,9 @@ export type TipoPlan =
| 'Doctorado' | 'Doctorado'
| 'Especialidad' | 'Especialidad'
export type TipoAsignatura = 'obligatoria' | 'optativa' | 'troncal' export type TipoMateria = 'obligatoria' | 'optativa' | 'troncal'
export type AsignaturaStatus = 'borrador' | 'revisada' | 'aprobada' export type MateriaStatus = 'borrador' | 'revisada' | 'aprobada'
export interface Facultad { export interface Facultad {
id: string id: string
@@ -36,15 +36,15 @@ export interface LineaCurricular {
color?: string color?: string
} }
export interface Asignatura { export interface Materia {
id: string id: string
clave: string clave: string
nombre: string nombre: string
creditos: number creditos: number
ciclo: number | null ciclo: number | null
lineaCurricularId: string | null lineaCurricularId: string | null
tipo: TipoAsignatura tipo: TipoMateria
estado: AsignaturaStatus estado: MateriaStatus
orden?: number orden?: number
hd: number // <--- Añadir hd: number // <--- Añadir
hi: number // <--- Añadir hi: number // <--- Añadir
@@ -103,7 +103,7 @@ export interface DocumentoPlan {
export type PlanTab = export type PlanTab =
| 'datos-generales' | 'datos-generales'
| 'mapa-curricular' | 'mapa-curricular'
| 'asignaturas' | 'materias'
| 'flujo' | 'flujo'
| 'ia' | 'ia'
| 'documento' | 'documento'

File diff suppressed because it is too large Load Diff