2 Commits

Author SHA1 Message Date
2185901c7a Merge pull request 'Refactorizar a Materias' (#55) from issue/50-se-prohbe-usar-la-palabra-materia into main
Reviewed-on: #55
2026-01-30 15:44:17 +00:00
d0b05256b0 refactor: rename Materia to Asignatura across the codebase
- Updated type definitions and interfaces to replace 'Materia' with 'Asignatura'.
- Refactored components and routes to reflect the new naming convention.
- Adjusted related types and constants for consistency.
- Removed the old Materia type definition and added Asignatura type definition.
- Ensured all references in UI components and logic are updated accordingly.

fix #50
2026-01-30 08:13:30 -06:00
20 changed files with 693 additions and 693 deletions

View File

@@ -8,6 +8,7 @@
"@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",
@@ -266,6 +267,8 @@
"@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

@@ -12,9 +12,13 @@ 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 { IAMateriaTab } from './IAMateriaTab' import { IAAsignaturaTab } from './IAAsignaturaTab'
import type { CampoEstructura, IAMessage, IASugerencia } from '@/types/materia' import type {
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'
@@ -30,10 +34,10 @@ import {
} from '@/components/ui/tooltip' } from '@/components/ui/tooltip'
import { useSubject } from '@/data/hooks/useSubjects' import { useSubject } from '@/data/hooks/useSubjects'
import { import {
mockMateria, mockAsignatura,
mockEstructura, mockEstructura,
mockDocumentoSep, mockDocumentoSep,
} from '@/data/mockMateriaData' } from '@/data/mockAsignaturaData'
export interface BibliografiaEntry { export interface BibliografiaEntry {
id: string id: string
@@ -101,10 +105,10 @@ function EditableHeaderField({
export const Route = createFileRoute( export const Route = createFileRoute(
'/planes/$planId/asignaturas/$asignaturaId', '/planes/$planId/asignaturas/$asignaturaId',
)({ )({
component: MateriaDetailPage, component: AsignaturaDetailPage,
}) })
export default function MateriaDetailPage() { export default function AsignaturaDetailPage() {
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({
@@ -121,7 +125,7 @@ export default function MateriaDetailPage() {
const [campos, setCampos] = useState<Array<CampoEstructura>>([]) const [campos, setCampos] = useState<Array<CampoEstructura>>([])
const [activeTab, setActiveTab] = useState('datos') const [activeTab, setActiveTab] = useState('datos')
// Dentro de MateriaDetailPage // Dentro de AsignaturaDetailPage
const [headerData, setHeaderData] = useState({ const [headerData, setHeaderData] = useState({
codigo: '', codigo: '',
nombre: '', nombre: '',
@@ -315,7 +319,7 @@ export default function MateriaDetailPage() {
<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 materia</TabsTrigger> <TabsTrigger value="ia">IA de la asignatura</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>
@@ -348,7 +352,7 @@ export default function MateriaDetailPage() {
</TabsContent> </TabsContent>
<TabsContent value="ia"> <TabsContent value="ia">
<IAMateriaTab <IAAsignaturaTab
campos={campos} campos={campos}
datosGenerales={datosGenerales} datosGenerales={datosGenerales}
messages={messages} messages={messages}
@@ -366,7 +370,7 @@ export default function MateriaDetailPage() {
<TabsContent value="sep"> <TabsContent value="sep">
<DocumentoSEPTab <DocumentoSEPTab
documento={mockDocumentoSep} documento={mockDocumentoSep}
materia={mockMateria} asignatura={mockAsignatura}
estructura={mockEstructura} estructura={mockEstructura}
datosGenerales={datosGenerales} datosGenerales={datosGenerales}
onRegenerate={handleRegenerateDocument} onRegenerate={handleRegenerateDocument}

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/mockMateriaData'; // import { mockLibraryResources } from '@/data/mockAsignaturaData';
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: loadinmateria } = const { data: bibliografia2, isLoading: loadinasignatura } =
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 {
DocumentoMateria, DocumentoAsignatura,
Materia, Asignatura,
MateriaStructure, AsignaturaStructure,
} from '@/types/materia' } from '@/types/asignatura'
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: DocumentoMateria | null documento: DocumentoAsignatura | null
materia: Materia asignatura: Asignatura
estructura: MateriaStructure estructura: AsignaturaStructure
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,
materia, asignatura,
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 materia. La versión anterior quedará en el actuales de la asignatura. 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 - {materia.clave} Programa de Estudios - {asignatura.clave}
</span> </span>
</div> </div>
<Badge variant="outline">Versión {documento.version}</Badge> <Badge variant="outline">Versión {documento.version}</Badge>
@@ -155,28 +155,29 @@ 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">
{materia.nombre} {asignatura.nombre}
</h1> </h1>
<p className="text-muted-foreground text-sm"> <p className="text-muted-foreground text-sm">
Clave: {materia.clave} | Créditos:{' '} Clave: {asignatura.clave} | Créditos:{' '}
{materia.creditos || 'N/A'} {asignatura.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> {materia.carrera} <strong>Carrera:</strong> {asignatura.carrera}
</p> </p>
<p> <p>
<strong>Facultad:</strong> {materia.facultad} <strong>Facultad:</strong> {asignatura.facultad}
</p> </p>
<p> <p>
<strong>Plan de estudios:</strong> {materia.planNombre} <strong>Plan de estudios:</strong>{' '}
{asignatura.planNombre}
</p> </p>
{materia.ciclo && ( {asignatura.ciclo && (
<p> <p>
<strong>Ciclo:</strong> {materia.ciclo} <strong>Ciclo:</strong> {asignatura.ciclo}
</p> </p>
)} )}
</div> </div>

View File

@@ -13,7 +13,11 @@ import {
} from 'lucide-react' } from 'lucide-react'
import { useState, useEffect, useRef, useMemo } from 'react' import { useState, useEffect, useRef, useMemo } from 'react'
import type { IAMessage, IASugerencia, CampoEstructura } from '@/types/materia' import type {
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'
@@ -22,7 +26,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 materia // Tipos importados de tu archivo de asignatura
const PRESETS = [ const PRESETS = [
{ {
@@ -35,7 +39,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 materia...', prompt: 'Genera un desglose de temas para esta asignatura...',
}, },
{ {
id: 'actividades', id: 'actividades',
@@ -57,7 +61,7 @@ interface SelectedField {
value: string value: string
} }
interface IAMateriaTabProps { interface IAAsignaturaTabProps {
campos: Array<CampoEstructura> campos: Array<CampoEstructura>
datosGenerales: Record<string, any> datosGenerales: Record<string, any>
messages: Array<IAMessage> messages: Array<IAMessage>
@@ -66,14 +70,14 @@ interface IAMateriaTabProps {
onRejectSuggestion: (messageId: string) => void onRejectSuggestion: (messageId: string) => void
} }
export function IAMateriaTab({ export function IAAsignaturaTab({
campos, campos,
datosGenerales, datosGenerales,
messages, messages,
onSendMessage, onSendMessage,
onAcceptSuggestion, onAcceptSuggestion,
onRejectSuggestion, onRejectSuggestion,
}: IAMateriaTabProps) { }: IAAsignaturaTabProps) {
const routerState = useRouterState() const routerState = useRouterState()
// ESTADOS PRINCIPALES (Igual que en Planes) // ESTADOS PRINCIPALES (Igual que en Planes)
@@ -83,7 +87,7 @@ export function IAMateriaTab({
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 materia para el menú // 1. Transformar datos de la asignatura 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"]
@@ -105,7 +109,7 @@ export function IAMateriaTab({
}) })
}, [campos, datosGenerales]) }, [campos, datosGenerales])
// 2. Manejar el estado inicial si viene de "Datos de Materia" (Prefill) // 2. Manejar el estado inicial si viene de "Datos de Asignatura" (Prefill)
useEffect(() => { useEffect(() => {
const state = routerState.location.state as any const state = routerState.location.state as any
@@ -244,7 +248,7 @@ export function IAMateriaTab({
{msg.content} {msg.content}
</div> </div>
{/* Renderizado de Sugerencias (Homologado con lógica de Materia) */} {/* Renderizado de Sugerencias (Homologado con lógica de Asignatura) */}
{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">
@@ -302,7 +306,7 @@ export function IAMateriaTab({
{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 materia Seleccionar campo de asignatura
</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

@@ -1,45 +1,56 @@
import type { PostgrestError, AuthError, SupabaseClient } from "@supabase/supabase-js"; import type { Database } from '../types/database'
import type { Database } from "../types/database"; import type {
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>(data: T | null | undefined, message = "Respuesta vacía"): T { export function requireData<T>(
if (data === null || data === undefined) throw new ApiError(message); data: T | null | undefined,
return data; message = 'Respuesta vacía',
): T {
if (data === null || data === undefined) throw new ApiError(message)
return data
} }
export async function getUserIdOrThrow(supabase: SupabaseClient<Database>): Promise<string> { export async function getUserIdOrThrow(
const { data, error } = await supabase.auth.getUser(); supabase: SupabaseClient<Database>,
throwIfError(error); ): Promise<string> {
if (!data?.user?.id) throw new ApiError("No hay sesión activa (auth)."); const { data, error } = await supabase.auth.getUser()
return data.user.id; throwIfError(error)
if (!data?.user?.id) throw new ApiError('No hay sesión activa (auth).')
return data.user.id
} }
export function buildRange(limit?: number, offset?: number): { from?: number; to?: number } { export function buildRange(
if (!limit) return {}; limit?: number,
const from = Math.max(0, offset ?? 0); offset?: number,
const to = from + Math.max(1, limit) - 1; ): { from?: number; to?: number } {
return { from, to }; if (!limit) return {}
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/materia */ /** “temporal” = evidencia usada para generar plan/asignatura */
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

@@ -333,7 +333,7 @@ export async function plans_import_from_files(payload: {
} }
archivoWordPlanId: UUID archivoWordPlanId: UUID
archivoMapaExcelId?: UUID | null archivoMapaExcelId?: UUID | null
archivoMateriasExcelId?: UUID | null archivoAsignaturasExcelId?: 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,35 +1,35 @@
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 { import type {
Asignatura, Asignatura,
BibliografiaAsignatura, BibliografiaAsignatura,
CambioAsignatura, CambioAsignatura,
TipoAsignatura, TipoAsignatura,
UUID, UUID,
} from "../types/domain"; } from '../types/domain'
import type { DocumentoResult } from "./plans.api"; 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,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, 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,
@@ -38,144 +38,170 @@ export async function subjects_get(subjectId: UUID): Promise<Asignatura> {
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, "Materia no encontrada."); return requireData(data, 'Asignatura no encontrada.')
} }
export async function subjects_history(subjectId: UUID): Promise<CambioAsignatura[]> { export async function subjects_history(
const supabase = supabaseBrowser(); subjectId: UUID,
): Promise<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(subjectId: UUID): Promise<BibliografiaAsignatura[]> { export async function subjects_bibliografia_list(
const supabase = supabaseBrowser(); subjectId: UUID,
): Promise<BibliografiaAsignatura[]> {
const supabase = supabaseBrowser()
const { data, error } = await supabase const { data, error } = await supabase
.from("bibliografia_asignatura") .from('bibliografia_asignatura')
.select("id,asignatura_id,tipo,cita,tipo_fuente,biblioteca_item_id,creado_por,creado_en,actualizado_en") .select(
.eq("asignatura_id", subjectId) 'id,asignatura_id,tipo,cita,tipo_fuente,biblioteca_item_id,creado_por,creado_en,actualizado_en',
.order("tipo", { ascending: true }) )
.order("creado_en", { ascending: true }); .eq('asignatura_id', subjectId)
.order('tipo', { ascending: true })
.order('creado_en', { ascending: true })
throwIfError(error); throwIfError(error)
return data ?? []; return data ?? []
} }
/** Wizard: crear materia manual (Edge Function) */ /** Wizard: crear asignatura 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(payload: SubjectsCreateManualInput): Promise<Asignatura> { export async function subjects_create_manual(
return invokeEdge<Asignatura>(EDGE.subjects_create_manual, payload); payload: SubjectsCreateManualInput,
): 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?: UUID[]; archivosExistentesIds?: UUID[]
repositoriosIds?: UUID[]; repositoriosIds?: UUID[]
archivosAdhocIds?: 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: { planId: UUID; jsonMateria: any }): Promise<Asignatura> { export async function subjects_persist_from_ai(payload: {
return invokeEdge<Asignatura>(EDGE.subjects_persist_from_ai, payload); planId: UUID
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: {
materiaOrigenId: UUID; asignaturaOrigenId: 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
archivoWordMateriaId: UUID; archivoWordAsignaturaId: UUID
archivosAdicionalesIds?: 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(subjectId: UUID, patch: SubjectsUpdateFieldsPatch): Promise<Asignatura> { export async function subjects_update_fields(
return invokeEdge<Asignatura>(EDGE.subjects_update_fields, { subjectId, patch }); subjectId: UUID,
patch: SubjectsUpdateFieldsPatch,
): Promise<Asignatura> {
return invokeEdge<Asignatura>(EDGE.subjects_update_fields, {
subjectId,
patch,
})
} }
export async function subjects_update_contenido(subjectId: UUID, unidades: any[]): Promise<Asignatura> { export async function subjects_update_contenido(
return invokeEdge<Asignatura>(EDGE.subjects_update_contenido, { subjectId, unidades }); subjectId: UUID,
unidades: 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, { subjectId, entries }); return invokeEdge<{ ok: true }>(EDGE.subjects_update_bibliografia, {
subjectId,
entries,
})
} }
/** Documento SEP materia */ /** Documento SEP asignatura */
/* export type DocumentoResult = { /* export type DocumentoResult = {
archivoId: UUID; archivoId: UUID;
signedUrl: string; signedUrl: string;
@@ -183,10 +209,18 @@ export async function subjects_update_bibliografia(
nombre?: string; nombre?: string;
}; */ }; */
export async function subjects_generate_document(subjectId: UUID): Promise<DocumentoResult> { export async function subjects_generate_document(
return invokeEdge<DocumentoResult>(EDGE.subjects_generate_document, { subjectId }); subjectId: UUID,
): Promise<DocumentoResult> {
return invokeEdge<DocumentoResult>(EDGE.subjects_generate_document, {
subjectId,
})
} }
export async function subjects_get_document(subjectId: UUID): Promise<DocumentoResult | null> { export async function subjects_get_document(
return invokeEdge<DocumentoResult | null>(EDGE.subjects_get_document, { subjectId }); subjectId: UUID,
): 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; jsonMateria: any }) => mutationFn: (payload: { planId: UUID; jsonAsignatura: 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 {
Materia, Asignatura,
MateriaStructure, AsignaturaStructure,
UnidadTematica, UnidadTematica,
BibliografiaEntry, BibliografiaEntry,
CambioMateria, CambioAsignatura,
DocumentoMateria, DocumentoAsignatura,
LibraryResource LibraryResource,
} from '@/types/materia'; } from '@/types/asignatura'
export const mockMateria: Materia = { export const mockAsignatura: Asignatura = {
id: '1', id: '1',
nombre: 'Inteligencia Artificial Aplicada', nombre: 'Inteligencia Artificial Aplicada',
clave: 'IAA-401', clave: 'IAA-401',
@@ -20,9 +20,9 @@ export const mockMateria: Materia = {
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: MateriaStructure = { export const mockEstructura: AsignaturaStructure = {
id: 'estructura-1', id: 'estructura-1',
nombre: 'Plantilla SEP Licenciatura', nombre: 'Plantilla SEP Licenciatura',
campos: [ campos: [
@@ -31,7 +31,7 @@ export const mockEstructura: MateriaStructure = {
nombre: 'Objetivo General', nombre: 'Objetivo General',
tipo: 'texto_largo', tipo: 'texto_largo',
obligatorio: true, obligatorio: true,
descripcion: 'Describe el propósito principal de la materia', descripcion: 'Describe el propósito principal de la asignatura',
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: MateriaStructure = {
nombre: 'Justificación', nombre: 'Justificación',
tipo: 'texto_largo', tipo: 'texto_largo',
obligatorio: true, obligatorio: true,
descripcion: 'Relevancia de la materia en el plan de estudios', descripcion: 'Relevancia de la asignatura 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: 'Materias previas requeridas', descripcion: 'Asignaturas previas requeridas',
}, },
{ {
id: 'estrategias_didacticas', id: 'estrategias_didacticas',
@@ -77,27 +77,49 @@ export const mockEstructura: MateriaStructure = {
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: '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.', objetivo_general:
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', '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.',
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.', competencias:
requisitos: 'Programación Avanzada (PAV-301), Matemáticas Discretas (MAT-201)', '• 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',
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', justificacion:
evaluacion: '• Exámenes parciales: 30%\n• Proyecto integrador: 35%\n• Prácticas de laboratorio: 20%\n• Participación y tareas: 15%', '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.',
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.', requisitos:
}; '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: UnidadTematica[] = [ export const mockContenidoTematico: Array<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-2', nombre: 'Tipos de IA y aplicaciones', descripcion: 'IA débil, fuerte y superinteligencia', horasEstimadas: 3 }, id: 'tema-1-1',
{ id: 'tema-1-3', nombre: 'Ética en IA', descripcion: 'Consideraciones éticas y responsabilidad', horasEstimadas: 2 }, nombre: 'Historia y evolución de la IA',
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,
},
], ],
}, },
{ {
@@ -105,9 +127,24 @@ export const mockContenidoTematico: 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-2', nombre: 'Aprendizaje no supervisado', descripcion: 'Clustering y reducción de dimensionalidad', horasEstimadas: 5 }, id: 'tema-2-1',
{ id: 'tema-2-3', nombre: 'Evaluación de modelos', descripcion: 'Métricas y validación cruzada', horasEstimadas: 4 }, nombre: 'Aprendizaje supervisado',
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,
},
], ],
}, },
{ {
@@ -115,10 +152,30 @@ export const mockContenidoTematico: 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-2', nombre: 'Redes convolucionales (CNN)', descripcion: 'Procesamiento de imágenes', horasEstimadas: 6 }, id: 'tema-3-1',
{ id: 'tema-3-3', nombre: 'Redes recurrentes (RNN)', descripcion: 'Procesamiento de secuencias', horasEstimadas: 5 }, nombre: 'Redes neuronales artificiales',
{ id: 'tema-3-4', nombre: 'Transformers y atención', descripcion: 'Arquitecturas modernas', horasEstimadas: 6 }, descripcion: 'Perceptrón y backpropagation',
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,
},
], ],
}, },
{ {
@@ -126,14 +183,29 @@ export const mockContenidoTematico: 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-2', nombre: 'Visión por computadora', descripcion: 'Detección y reconocimiento', horasEstimadas: 5 }, id: 'tema-4-1',
{ id: 'tema-4-3', nombre: 'Sistemas de recomendación', descripcion: 'Filtrado colaborativo y contenido', horasEstimadas: 4 }, nombre: 'Procesamiento de lenguaje natural',
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: BibliografiaEntry[] = [ export const mockBibliografia: Array<BibliografiaEntry> = [
{ {
id: 'bib-1', id: 'bib-1',
tipo: 'BASICA', tipo: 'BASICA',
@@ -153,13 +225,14 @@ export const mockBibliografia: 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: 'Hands-On Machine Learning with Scikit-Learn, Keras, and TensorFlow', titulo:
'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',
@@ -187,9 +260,9 @@ export const mockBibliografia: BibliografiaEntry[] = [
disponible: false, disponible: false,
}, },
}, },
]; ]
export const mockHistorial: CambioMateria[] = [ export const mockHistorial: Array<CambioAsignatura> = [
{ {
id: 'cambio-1', id: 'cambio-1',
tipo: 'datos', tipo: 'datos',
@@ -228,17 +301,17 @@ export const mockHistorial: CambioMateria[] = [
usuario: 'Sistema', usuario: 'Sistema',
fecha: new Date('2024-12-06T11:30:00'), fecha: new Date('2024-12-06T11:30:00'),
}, },
]; ]
export const mockDocumentoSep: DocumentoMateria = { export const mockDocumentoSep: DocumentoAsignatura = {
id: 'doc-1', id: 'doc-1',
materiaId: '1', asignaturaId: '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: LibraryResource[] = [ export const mockLibraryResources: Array<LibraryResource> = [
{ {
id: 'lib-1', id: 'lib-1',
titulo: 'Artificial Intelligence: A Modern Approach', titulo: 'Artificial Intelligence: A Modern Approach',
@@ -251,9 +324,10 @@ export const mockLibraryResources: LibraryResource[] = [
}, },
{ {
id: 'lib-2', id: 'lib-2',
titulo: 'Hands-On Machine Learning with Scikit-Learn, Keras, and TensorFlow', titulo:
'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',
@@ -299,4 +373,4 @@ export const mockLibraryResources: 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: "Material didáctico Web.pdf", nombre: 'Asignatural 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: "Materiales ISC 2024", nombre: 'Asignaturales 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

@@ -1,137 +0,0 @@
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 { Materia } from '@/types/plan' import type { Asignatura } 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'
@@ -46,7 +46,7 @@ const tipoConfig: Record<string, { label: string; className: string }> = {
} }
// --- Mapeadores de API --- // --- Mapeadores de API ---
const mapAsignaturas = (asigApi: Array<any> = []): Array<Materia> => { const mapAsignaturas = (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,
@@ -63,10 +63,10 @@ const mapAsignaturas = (asigApi: Array<any> = []): Array<Materia> => {
} }
export const Route = createFileRoute('/planes/$planId/_detalle/asignaturas')({ export const Route = createFileRoute('/planes/$planId/_detalle/asignaturas')({
component: MateriasPage, component: AsignaturasPage,
}) })
function MateriasPage() { function AsignaturasPage() {
const { planId } = Route.useParams() const { planId } = Route.useParams()
const navigate = useNavigate() const navigate = useNavigate()
@@ -82,13 +82,13 @@ function MateriasPage() {
const [filterLinea, setFilterLinea] = useState<string>('all') const [filterLinea, setFilterLinea] = useState<string>('all')
// 3. Procesamiento de datos // 3. Procesamiento de datos
const materias = useMemo( const asignaturas = useMemo(
() => mapAsignaturas(asignaturasApi), () => mapAsignaturas(asignaturasApi),
[asignaturasApi], [asignaturasApi],
) )
const lineas = useMemo(() => lineasApi || [], [lineasApi]) const lineas = useMemo(() => lineasApi || [], [lineasApi])
const filteredMaterias = materias.filter((m) => { const filteredAsignaturas = asignaturas.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())
@@ -119,11 +119,11 @@ function MateriasPage() {
<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">
Materias del Plan Asignaturas del Plan
</h2> </h2>
<p className="text-muted-foreground mt-1 text-sm"> <p className="text-muted-foreground mt-1 text-sm">
{materias.length} materias en total {filteredMaterias.length}{' '} {asignaturas.length} asignaturas en total {' '}
filtradas {filteredAsignaturas.length} filtradas
</p> </p>
</div> </div>
@@ -132,7 +132,7 @@ function MateriasPage() {
<Copy className="mr-2 h-4 w-4" /> Clonar <Copy className="mr-2 h-4 w-4" /> Clonar
</Button> </Button>
<Button className="bg-emerald-700 hover:bg-emerald-800"> <Button className="bg-emerald-700 hover:bg-emerald-800">
<Plus className="mr-2 h-4 w-4" /> Nueva Materia <Plus className="mr-2 h-4 w-4" /> Nueva Asignatura
</Button> </Button>
</div> </div>
</div> </div>
@@ -207,12 +207,12 @@ function MateriasPage() {
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{filteredMaterias.length === 0 ? ( {filteredAsignaturas.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 materias</p> <p className="font-medium">No se encontraron asignaturas</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>
@@ -220,59 +220,59 @@ function MateriasPage() {
</TableCell> </TableCell>
</TableRow> </TableRow>
) : ( ) : (
filteredMaterias.map((materia) => ( filteredAsignaturas.map((asignatura) => (
<TableRow <TableRow
key={materia.id} key={asignatura.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: materia.id, // 👈 puede ser índice, consecutivo o slug asignaturaId: asignatura.id, // 👈 puede ser índice, consecutivo o slug
}, },
state: { state: {
realId: materia.id, // 👈 ID largo oculto realId: asignatura.id, // 👈 ID largo oculto
asignaturaId: materia.id, asignaturaId: asignatura.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">
{materia.clave} {asignatura.clave}
</TableCell> </TableCell>
<TableCell className="font-semibold text-slate-700"> <TableCell className="font-semibold text-slate-700">
{materia.nombre} {asignatura.nombre}
</TableCell> </TableCell>
<TableCell className="text-center font-medium"> <TableCell className="text-center font-medium">
{materia.creditos} {asignatura.creditos}
</TableCell> </TableCell>
<TableCell className="text-center"> <TableCell className="text-center">
{materia.ciclo ? ( {asignatura.ciclo ? (
<Badge variant="outline" className="font-normal"> <Badge variant="outline" className="font-normal">
Ciclo {materia.ciclo} Ciclo {asignatura.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(materia.lineaCurricularId)} {getLineaNombre(asignatura.lineaCurricularId)}
</TableCell> </TableCell>
<TableCell> <TableCell>
<Badge <Badge
variant="outline" variant="outline"
className={`capitalize shadow-sm ${tipoConfig[materia.tipo]?.className}`} className={`capitalize shadow-sm ${tipoConfig[asignatura.tipo]?.className}`}
> >
{tipoConfig[materia.tipo]?.label} {tipoConfig[asignatura.tipo]?.label}
</Badge> </Badge>
</TableCell> </TableCell>
<TableCell> <TableCell>
<Badge <Badge
variant="outline" variant="outline"
className={`capitalize shadow-sm ${statusConfig[materia.estado]?.className}`} className={`capitalize shadow-sm ${statusConfig[asignatura.estado]?.className}`}
> >
{statusConfig[materia.estado]?.label} {statusConfig[asignatura.estado]?.label}
</Badge> </Badge>
</TableCell> </TableCell>
<TableCell> <TableCell>

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 { Materia, LineaCurricular } from '@/types/plan' import type { Asignatura, 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,7 +47,9 @@ const mapLineasToLineaCurricular = (
})) }))
} }
const mapAsignaturasToMaterias = (asigApi: Array<any> = []): Array<Materia> => { const mapAsignaturasToAsignaturas = (
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,
@@ -104,13 +106,13 @@ function StatItem({
) )
} }
function MateriaCardItem({ function AsignaturaCardItem({
materia, asignatura,
onDragStart, onDragStart,
isDragging, isDragging,
onClick, onClick,
}: { }: {
materia: Materia asignatura: Asignatura
onDragStart: (e: React.DragEvent, id: string) => void onDragStart: (e: React.DragEvent, id: string) => void
isDragging: boolean isDragging: boolean
onClick: () => void onClick: () => void
@@ -118,7 +120,7 @@ function MateriaCardItem({
return ( return (
<button <button
draggable draggable
onDragStart={(e) => onDragStart(e, materia.id)} onDragStart={(e) => onDragStart(e, asignatura.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
@@ -128,21 +130,21 @@ function MateriaCardItem({
> >
<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">
{materia.clave} {asignatura.clave}
</span> </span>
<Badge <Badge
variant="outline" variant="outline"
className={`px-1 py-0 text-[9px] uppercase ${statusBadge[materia.estado] || ''}`} className={`px-1 py-0 text-[9px] uppercase ${statusBadge[asignatura.estado] || ''}`}
> >
{materia.estado} {asignatura.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">
{materia.nombre} {asignatura.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">
{materia.creditos} CR HD:{materia.hd} HI:{materia.hi} {asignatura.creditos} CR HD:{asignatura.hd} HI:{asignatura.hi}
</span> </span>
<GripVertical <GripVertical
size={12} size={12}
@@ -166,11 +168,14 @@ 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 [materias, setMaterias] = useState<Array<Materia>>([]) const [asignaturas, setAsignaturas] = useState<Array<Asignatura>>([])
const [lineas, setLineas] = useState<Array<LineaCurricular>>([]) const [lineas, setLineas] = useState<Array<LineaCurricular>>([])
const [draggedMateria, setDraggedMateria] = useState<string | null>(null) const [draggedAsignatura, setDraggedAsignatura] = useState<string | null>(
null,
)
const [isEditModalOpen, setIsEditModalOpen] = useState(false) const [isEditModalOpen, setIsEditModalOpen] = useState(false)
const [selectedMateria, setSelectedMateria] = useState<Materia | null>(null) const [selectedAsignatura, setSelectedAsignatura] =
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
@@ -236,7 +241,8 @@ function MapaCurricularPage() {
// 3. Sincronizar API -> Estado Local // 3. Sincronizar API -> Estado Local
useEffect(() => { useEffect(() => {
if (asignaturasApi) setMaterias(mapAsignaturasToMaterias(asignaturasApi)) if (asignaturasApi)
setAsignaturas(mapAsignaturasToAsignaturas(asignaturasApi))
}, [asignaturasApi]) }, [asignaturasApi])
useEffect(() => { useEffect(() => {
@@ -247,23 +253,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<Materia | null>(null) const [editingData, setEditingData] = useState<Asignatura | null>(null)
// 1. FUNCION DE GUARDAR MODAL // 1. FUNCION DE GUARDAR MODAL
const handleSaveChanges = () => { const handleSaveChanges = () => {
if (!editingData) return if (!editingData) return
console.log(materias) console.log(asignaturas)
setMaterias((prev) => setAsignaturas((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 materias sin asignar // Cambiamos la condición: Mostramos la sección si hay asignaturas sin asignar
// O si simplemente queremos tener el "depósito" disponible. // O si simplemente queremos tener el "depósito" disponible.
const unassignedMaterias = materias.filter((m) => m.ciclo === null) const unassignedAsignaturas = asignaturas.filter((m) => m.ciclo === null)
// --- Lógica de Gestión --- // --- Lógica de Gestión ---
const agregarLinea = (nombre: string) => { const agregarLinea = (nombre: string) => {
@@ -272,7 +278,7 @@ function MapaCurricularPage() {
} }
const borrarLinea = (id: string) => { const borrarLinea = (id: string) => {
setMaterias((prev) => setAsignaturas((prev) =>
prev.map((m) => prev.map((m) =>
m.lineaCurricularId === id m.lineaCurricularId === id
? { ...m, ciclo: null, lineaCurricularId: null } ? { ...m, ciclo: null, lineaCurricularId: null }
@@ -284,7 +290,7 @@ function MapaCurricularPage() {
// --- Selectores/Cálculos --- // --- Selectores/Cálculos ---
const getTotalesCiclo = (ciclo: number) => { const getTotalesCiclo = (ciclo: number) => {
return materias return asignaturas
.filter((m) => m.ciclo === ciclo) .filter((m) => m.ciclo === ciclo)
.reduce( .reduce(
(acc, m) => ({ (acc, m) => ({
@@ -297,7 +303,7 @@ function MapaCurricularPage() {
} }
const getSubtotalLinea = (lineaId: string) => { const getSubtotalLinea = (lineaId: string) => {
return materias return asignaturas
.filter((m) => m.lineaCurricularId === lineaId && m.ciclo !== null) .filter((m) => m.lineaCurricularId === lineaId && m.ciclo !== null)
.reduce( .reduce(
(acc, m) => ({ (acc, m) => ({
@@ -310,7 +316,7 @@ function MapaCurricularPage() {
} }
const handleDragStart = (e: React.DragEvent, id: string) => { const handleDragStart = (e: React.DragEvent, id: string) => {
setDraggedMateria(id) setDraggedAsignatura(id)
e.dataTransfer.effectAllowed = 'move' e.dataTransfer.effectAllowed = 'move'
} }
const handleDragOver = (e: React.DragEvent) => e.preventDefault() const handleDragOver = (e: React.DragEvent) => e.preventDefault()
@@ -320,21 +326,21 @@ function MapaCurricularPage() {
lineaId: string | null, lineaId: string | null,
) => { ) => {
e.preventDefault() e.preventDefault()
if (draggedMateria) { if (draggedAsignatura) {
setMaterias((prev) => setAsignaturas((prev) =>
prev.map((m) => prev.map((m) =>
m.id === draggedMateria m.id === draggedAsignatura
? { ...m, ciclo, lineaCurricularId: lineaId } ? { ...m, ciclo, lineaCurricularId: lineaId }
: m, : m,
), ),
) )
setDraggedMateria(null) setDraggedAsignatura(null)
} }
} }
const stats = useMemo( const stats = useMemo(
() => () =>
materias.reduce( asignaturas.reduce(
(acc, m) => { (acc, m) => {
if (m.ciclo !== null) { if (m.ciclo !== null) {
acc.cr += m.creditos || 0 acc.cr += m.creditos || 0
@@ -345,7 +351,7 @@ function MapaCurricularPage() {
}, },
{ cr: 0, hd: 0, hi: 0 }, { cr: 0, hd: 0, hi: 0 },
), ),
[materias], [asignaturas],
) )
if (loadingAsig || loadingLineas) if (loadingAsig || loadingLineas)
@@ -358,14 +364,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 materias de la petición por línea y ciclo Organiza las asignaturas 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">
{materias.filter((m) => !m.ciclo).length > 0 && ( {asignaturas.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" />{' '}
{materias.filter((m) => !m.ciclo).length} sin asignar {asignaturas.filter((m) => !m.ciclo).length} sin asignar
</Badge> </Badge>
)} )}
<DropdownMenu> <DropdownMenu>
@@ -474,16 +480,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"
> >
{materias {asignaturas
.filter( .filter(
(m) => (m) =>
m.ciclo === ciclo && m.lineaCurricularId === linea.id, m.ciclo === ciclo && m.lineaCurricularId === linea.id,
) )
.map((m) => ( .map((m) => (
<MateriaCardItem <AsignaturaCardItem
key={m.id} key={m.id}
materia={m} asignatura={m}
isDragging={draggedMateria === m.id} isDragging={draggedAsignatura === m.id}
onDragStart={handleDragStart} onDragStart={handleDragStart}
onClick={() => { onClick={() => {
setEditingData(m) setEditingData(m)
@@ -534,35 +540,35 @@ function MapaCurricularPage() {
</div> </div>
</div> </div>
{/* Materias Sin Asignar */} {/* Asignaturas 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 / Materias sin asignar Bandeja de Entrada / Asignaturas sin asignar
</h3> </h3>
<Badge variant="secondary">{unassignedMaterias.length}</Badge> <Badge variant="secondary">{unassignedAsignaturas.length}</Badge>
</div> </div>
<p className="text-xs text-slate-400"> <p className="text-xs text-slate-400">
Arrastra una materia aquí para quitarla del mapa Arrastra una asignatura 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 ${
draggedMateria draggedAsignatura
? '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
> >
{unassignedMaterias.map((m) => ( {unassignedAsignaturas.map((m) => (
<div key={m.id} className="w-[200px]"> <div key={m.id} className="w-[200px]">
<MateriaCardItem <AsignaturaCardItem
materia={m} asignatura={m}
isDragging={draggedMateria === m.id} isDragging={draggedAsignatura === 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
@@ -571,9 +577,9 @@ function MapaCurricularPage() {
/> />
</div> </div>
))} ))}
{unassignedMaterias.length === 0 && ( {unassignedAsignaturas.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 materias pendientes. Arrastra una materia aquí para No hay asignaturas pendientes. Arrastra una asignatura aquí para
desasignarla. desasignarla.
</div> </div>
)} )}
@@ -585,7 +591,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 Materia Editar Asignatura
</DialogTitle> </DialogTitle>
</DialogHeader> </DialogHeader>
@@ -735,10 +741,10 @@ function MapaCurricularPage() {
</label> </label>
<Select> <Select>
<SelectTrigger> <SelectTrigger>
<SelectValue placeholder="Seleccionar materia..." /> <SelectValue placeholder="Seleccionar asignatura..." />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{materias.map((m) => ( {asignaturas.map((m) => (
<SelectItem key={m.id} value={m.clave}> <SelectItem key={m.id} value={m.clave}>
{m.nombre} {m.nombre}
</SelectItem> </SelectItem>

View File

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

View File

@@ -1,8 +1,8 @@
import MateriaDetailPage from '@/components/asignaturas/detalle/MateriaDetailPage' import AsignaturaDetailPage from '@/components/asignaturas/detalle/AsignaturaDetailPage'
import { createFileRoute } from '@tanstack/react-router' import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute( export const Route = createFileRoute(
'/planes/$planId/asignaturas/$asignaturaId' '/planes/$planId/asignaturas/$asignaturaId',
)({ )({
component: RouteComponent, component: RouteComponent,
}) })
@@ -12,7 +12,7 @@ function RouteComponent() {
return ( return (
<div> <div>
<MateriaDetailPage></MateriaDetailPage> <AsignaturaDetailPage></AsignaturaDetailPage>
</div> </div>
) )
} }

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

@@ -0,0 +1,119 @@
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
}

View File

@@ -1,119 +0,0 @@
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 TipoMateria = 'obligatoria' | 'optativa' | 'troncal' export type TipoAsignatura = 'obligatoria' | 'optativa' | 'troncal'
export type MateriaStatus = 'borrador' | 'revisada' | 'aprobada' export type AsignaturaStatus = '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 Materia { export interface Asignatura {
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: TipoMateria tipo: TipoAsignatura
estado: MateriaStatus estado: AsignaturaStatus
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'
| 'materias' | 'asignaturas'
| 'flujo' | 'flujo'
| 'ia' | 'ia'
| 'documento' | 'documento'