Refactorizar a Materias #55

Merged
javier.garrido merged 1 commits from issue/50-se-prohbe-usar-la-palabra-materia into main 2026-01-30 15:44:18 +00: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-checkbox": "^1.3.3",
"@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-dropdown-menu": "^2.1.16",
"@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-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-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 { DocumentoSEPTab } from './DocumentoSEPTab'
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 { Button } from '@/components/ui/button'
@@ -30,10 +34,10 @@ import {
} from '@/components/ui/tooltip'
import { useSubject } from '@/data/hooks/useSubjects'
import {
mockMateria,
mockAsignatura,
mockEstructura,
mockDocumentoSep,
} from '@/data/mockMateriaData'
} from '@/data/mockAsignaturaData'
export interface BibliografiaEntry {
id: string
@@ -101,10 +105,10 @@ function EditableHeaderField({
export const Route = createFileRoute(
'/planes/$planId/asignaturas/$asignaturaId',
)({
component: MateriaDetailPage,
component: AsignaturaDetailPage,
})
export default function MateriaDetailPage() {
export default function AsignaturaDetailPage() {
const routerState = useRouterState()
const state = routerState.location.state as any
const { asignaturaId } = useParams({
@@ -121,7 +125,7 @@ export default function MateriaDetailPage() {
const [campos, setCampos] = useState<Array<CampoEstructura>>([])
const [activeTab, setActiveTab] = useState('datos')
// Dentro de MateriaDetailPage
// Dentro de AsignaturaDetailPage
const [headerData, setHeaderData] = useState({
codigo: '',
nombre: '',
@@ -315,7 +319,7 @@ export default function MateriaDetailPage() {
<TabsTrigger value="datos">Datos generales</TabsTrigger>
<TabsTrigger value="contenido">Contenido temático</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="historial">Historial</TabsTrigger>
</TabsList>
@@ -348,7 +352,7 @@ export default function MateriaDetailPage() {
</TabsContent>
<TabsContent value="ia">
<IAMateriaTab
<IAAsignaturaTab
campos={campos}
datosGenerales={datosGenerales}
messages={messages}
@@ -366,7 +370,7 @@ export default function MateriaDetailPage() {
<TabsContent value="sep">
<DocumentoSEPTab
documento={mockDocumentoSep}
materia={mockMateria}
asignatura={mockAsignatura}
estructura={mockEstructura}
datosGenerales={datosGenerales}
onRegenerate={handleRegenerateDocument}

View File

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

View File

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

View File

@@ -13,7 +13,11 @@ import {
} from 'lucide-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 { Badge } from '@/components/ui/badge'
@@ -22,7 +26,7 @@ import { ScrollArea } from '@/components/ui/scroll-area'
import { Textarea } from '@/components/ui/textarea'
import { cn } from '@/lib/utils'
// Tipos importados de tu archivo de materia
// Tipos importados de tu archivo de asignatura
const PRESETS = [
{
@@ -35,7 +39,7 @@ const PRESETS = [
id: 'contenido-tematico',
label: 'Sugerir contenido',
icon: BookOpen,
prompt: 'Genera un desglose de temas para esta materia...',
prompt: 'Genera un desglose de temas para esta asignatura...',
},
{
id: 'actividades',
@@ -57,7 +61,7 @@ interface SelectedField {
value: string
}
interface IAMateriaTabProps {
interface IAAsignaturaTabProps {
campos: Array<CampoEstructura>
datosGenerales: Record<string, any>
messages: Array<IAMessage>
@@ -66,14 +70,14 @@ interface IAMateriaTabProps {
onRejectSuggestion: (messageId: string) => void
}
export function IAMateriaTab({
export function IAAsignaturaTab({
campos,
datosGenerales,
messages,
onSendMessage,
onAcceptSuggestion,
onRejectSuggestion,
}: IAMateriaTabProps) {
}: IAAsignaturaTabProps) {
const routerState = useRouterState()
// ESTADOS PRINCIPALES (Igual que en Planes)
@@ -83,7 +87,7 @@ export function IAMateriaTab({
const [isLoading, setIsLoading] = useState(false)
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(() => {
// Extraemos las claves directamente del objeto datosGenerales
// ["nombre", "descripcion", "perfil_de_egreso", "fines_de_aprendizaje_o_formacion"]
@@ -105,7 +109,7 @@ export function IAMateriaTab({
})
}, [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(() => {
const state = routerState.location.state as any
@@ -244,7 +248,7 @@ export function IAMateriaTab({
{msg.content}
</div>
{/* Renderizado de Sugerencias (Homologado con lógica de Materia) */}
{/* Renderizado de Sugerencias (Homologado con lógica de Asignatura) */}
{msg.sugerencia && !msg.sugerencia.aceptada && (
<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">
@@ -302,7 +306,7 @@ export function IAMateriaTab({
{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="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 className="max-h-64 overflow-y-auto p-1">
{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 {
constructor(
message: string,
public readonly code?: string,
public readonly details?: unknown,
public readonly hint?: string
public readonly hint?: string,
) {
super(message);
this.name = "ApiError";
super(message)
this.name = 'ApiError'
}
}
export function throwIfError(error: PostgrestError | AuthError | null): void {
if (!error) return;
const anyErr = error as any;
if (!error) return
const anyErr = error as any
throw new ApiError(
anyErr.message ?? "Error inesperado",
anyErr.message ?? 'Error inesperado',
anyErr.code,
anyErr.details,
anyErr.hint
);
anyErr.hint,
)
}
export function requireData<T>(data: T | null | undefined, message = "Respuesta vacía"): T {
if (data === null || data === undefined) throw new ApiError(message);
return data;
export function requireData<T>(
data: T | null | undefined,
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> {
const { data, error } = await supabase.auth.getUser();
throwIfError(error);
if (!data?.user?.id) throw new ApiError("No hay sesión activa (auth).");
return data.user.id;
export async function getUserIdOrThrow(
supabase: SupabaseClient<Database>,
): Promise<string> {
const { data, error } = await supabase.auth.getUser()
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 } {
if (!limit) return {};
const from = Math.max(0, offset ?? 0);
const to = from + Math.max(1, limit) - 1;
return { from, to };
export function buildRange(
limit?: number,
offset?: number,
): { from?: number; to?: number } {
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 type { UUID } from "../types/domain";
import { invokeEdge } from '../supabase/invokeEdge'
import type { UUID } from '../types/domain'
/**
* Metadata “canónica” para UI (archivo OpenAI + espejo en Supabase)
* Se apoya en tu tabla `archivos`.
*/
export type AppFile = {
id: UUID; // id interno (tabla archivos)
openai_file_id: string; // id OpenAI
nombre: string;
mime_type: string | null;
bytes: number | null;
id: UUID // id interno (tabla archivos)
openai_file_id: string // id OpenAI
nombre: string
mime_type: string | null
bytes: number | null
// espejo Supabase para preview/descarga
ruta_storage: string | null; // "bucket/path"
signed_url?: string | null;
ruta_storage: string | null // "bucket/path"
signed_url?: string | null
// auditoría/evidencia
temporal: boolean;
notas?: string | null;
temporal: boolean
notas?: string | null
subido_en: string;
};
subido_en: string
}
const EDGE = {
upload: "openai_files_upload",
remove: "openai_files_delete",
} as const;
upload: 'openai_files_upload',
remove: 'openai_files_delete',
} as const
/**
* 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 no, manda base64/bytes (según tu implementación).
*/
file: File;
file: File
/** “temporal” = evidencia usada para generar plan/materia */
temporal?: boolean;
/** “temporal” = evidencia usada para generar plan/asignatura */
temporal?: boolean
/** contexto para auditoría */
contexto?: {
planId?: UUID;
asignaturaId?: UUID;
motivo?: "WIZARD_PLAN" | "WIZARD_MATERIA" | "ADHOC";
};
planId?: UUID
asignaturaId?: UUID
motivo?: 'WIZARD_PLAN' | 'WIZARD_MATERIA' | 'ADHOC'
}
/** si quieres forzar espejo para preview siempre */
mirrorToSupabase?: boolean;
mirrorToSupabase?: boolean
}): Promise<AppFile> {
return invokeEdge<AppFile>(EDGE.upload, payload);
return invokeEdge<AppFile>(EDGE.upload, payload)
}
export async function openai_files_delete(payload: {
openaiFileId: string;
openaiFileId: string
/** si quieres borrar también espejo y registro */
hardDelete?: boolean;
hardDelete?: boolean
}): 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
archivoMapaExcelId?: UUID | null
archivoMateriasExcelId?: UUID | null
archivoAsignaturasExcelId?: UUID | null
}): Promise<PlanEstudio> {
return invokeEdge<PlanEstudio>(EDGE.plans_import_from_files, payload)
}

View File

@@ -1,35 +1,35 @@
import { supabaseBrowser } from "../supabase/client";
import { invokeEdge } from "../supabase/invokeEdge";
import { throwIfError, requireData } from "./_helpers";
import { supabaseBrowser } from '../supabase/client'
import { invokeEdge } from '../supabase/invokeEdge'
import { throwIfError, requireData } from './_helpers'
import type {
Asignatura,
BibliografiaAsignatura,
CambioAsignatura,
TipoAsignatura,
UUID,
} from "../types/domain";
import type { DocumentoResult } from "./plans.api";
} from '../types/domain'
import type { DocumentoResult } from './plans.api'
const EDGE = {
subjects_create_manual: "subjects_create_manual",
ai_generate_subject: "ai_generate_subject",
subjects_persist_from_ai: "subjects_persist_from_ai",
subjects_clone_from_existing: "subjects_clone_from_existing",
subjects_import_from_file: "subjects_import_from_file",
subjects_create_manual: 'subjects_create_manual',
ai_generate_subject: 'ai_generate_subject',
subjects_persist_from_ai: 'subjects_persist_from_ai',
subjects_clone_from_existing: 'subjects_clone_from_existing',
subjects_import_from_file: 'subjects_import_from_file',
subjects_update_fields: "subjects_update_fields",
subjects_update_contenido: "subjects_update_contenido",
subjects_update_bibliografia: "subjects_update_bibliografia",
subjects_update_fields: 'subjects_update_fields',
subjects_update_contenido: 'subjects_update_contenido',
subjects_update_bibliografia: 'subjects_update_bibliografia',
subjects_generate_document: "subjects_generate_document",
subjects_get_document: "subjects_get_document",
} as const;
subjects_generate_document: 'subjects_generate_document',
subjects_get_document: 'subjects_get_document',
} as const
export async function subjects_get(subjectId: UUID): Promise<Asignatura> {
const supabase = supabaseBrowser();
const supabase = supabaseBrowser()
const { data, error } = await supabase
.from("asignaturas")
.from('asignaturas')
.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,
@@ -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))
),
estructuras_asignatura(id,nombre,version,definicion)
`
`,
)
.eq("id", subjectId)
.single();
.eq('id', subjectId)
.single()
throwIfError(error);
return requireData(data, "Materia no encontrada.");
throwIfError(error)
return requireData(data, 'Asignatura no encontrada.')
}
export async function subjects_history(subjectId: UUID): Promise<CambioAsignatura[]> {
const supabase = supabaseBrowser();
export async function subjects_history(
subjectId: UUID,
): Promise<CambioAsignatura[]> {
const supabase = supabaseBrowser()
const { data, error } = await supabase
.from("cambios_asignatura")
.from('cambios_asignatura')
.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)
.order("cambiado_en", { ascending: false });
.eq('asignatura_id', subjectId)
.order('cambiado_en', { ascending: false })
throwIfError(error);
return data ?? [];
throwIfError(error)
return data ?? []
}
export async function subjects_bibliografia_list(subjectId: UUID): Promise<BibliografiaAsignatura[]> {
const supabase = supabaseBrowser();
export async function subjects_bibliografia_list(
subjectId: UUID,
): Promise<BibliografiaAsignatura[]> {
const supabase = supabaseBrowser()
const { data, error } = await supabase
.from("bibliografia_asignatura")
.select("id,asignatura_id,tipo,cita,tipo_fuente,biblioteca_item_id,creado_por,creado_en,actualizado_en")
.eq("asignatura_id", subjectId)
.order("tipo", { ascending: true })
.order("creado_en", { ascending: true });
.from('bibliografia_asignatura')
.select(
'id,asignatura_id,tipo,cita,tipo_fuente,biblioteca_item_id,creado_por,creado_en,actualizado_en',
)
.eq('asignatura_id', subjectId)
.order('tipo', { ascending: true })
.order('creado_en', { ascending: true })
throwIfError(error);
return data ?? [];
throwIfError(error)
return data ?? []
}
/** Wizard: crear materia manual (Edge Function) */
/** Wizard: crear asignatura manual (Edge Function) */
export type SubjectsCreateManualInput = {
planId: UUID;
planId: UUID
datosBasicos: {
nombre: string;
clave?: string;
tipo: TipoAsignatura;
creditos: number;
horasSemana?: number;
estructuraId: UUID;
};
};
nombre: string
clave?: string
tipo: TipoAsignatura
creditos: number
horasSemana?: number
estructuraId: UUID
}
}
export async function subjects_create_manual(payload: SubjectsCreateManualInput): Promise<Asignatura> {
return invokeEdge<Asignatura>(EDGE.subjects_create_manual, payload);
export async function subjects_create_manual(
payload: SubjectsCreateManualInput,
): Promise<Asignatura> {
return invokeEdge<Asignatura>(EDGE.subjects_create_manual, payload)
}
export async function ai_generate_subject(payload: {
planId: UUID;
planId: UUID
datosBasicos: {
nombre: string;
clave?: string;
tipo: TipoAsignatura;
creditos: number;
horasSemana?: number;
estructuraId: UUID;
};
nombre: string
clave?: string
tipo: TipoAsignatura
creditos: number
horasSemana?: number
estructuraId: UUID
}
iaConfig: {
descripcionEnfoque: string;
notasAdicionales?: string;
archivosExistentesIds?: UUID[];
repositoriosIds?: UUID[];
archivosAdhocIds?: UUID[];
usarMCP?: boolean;
};
descripcionEnfoque: string
notasAdicionales?: string
archivosExistentesIds?: UUID[]
repositoriosIds?: UUID[]
archivosAdhocIds?: UUID[]
usarMCP?: boolean
}
}): 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> {
return invokeEdge<Asignatura>(EDGE.subjects_persist_from_ai, payload);
export async function 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: {
materiaOrigenId: UUID;
planDestinoId: UUID;
asignaturaOrigenId: UUID
planDestinoId: UUID
overrides?: Partial<{
nombre: string;
codigo: string;
tipo: TipoAsignatura;
creditos: number;
horas_semana: number;
}>;
nombre: string
codigo: string
tipo: TipoAsignatura
creditos: number
horas_semana: number
}>
}): 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: {
planId: UUID;
archivoWordMateriaId: UUID;
archivosAdicionalesIds?: UUID[];
planId: UUID
archivoWordAsignaturaId: UUID
archivosAdicionalesIds?: UUID[]
}): 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) */
export type SubjectsUpdateFieldsPatch = Partial<{
codigo: string | null;
nombre: string;
tipo: TipoAsignatura;
creditos: number;
horas_semana: number | null;
numero_ciclo: number | null;
linea_plan_id: UUID | null;
codigo: string | null
nombre: string
tipo: TipoAsignatura
creditos: number
horas_semana: number | null
numero_ciclo: number | 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> {
return invokeEdge<Asignatura>(EDGE.subjects_update_fields, { subjectId, patch });
export async function subjects_update_fields(
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> {
return invokeEdge<Asignatura>(EDGE.subjects_update_contenido, { subjectId, unidades });
export async function subjects_update_contenido(
subjectId: UUID,
unidades: any[],
): Promise<Asignatura> {
return invokeEdge<Asignatura>(EDGE.subjects_update_contenido, {
subjectId,
unidades,
})
}
export type BibliografiaUpsertInput = Array<{
id?: UUID;
tipo: "BASICA" | "COMPLEMENTARIA";
cita: string;
tipo_fuente?: "MANUAL" | "BIBLIOTECA";
biblioteca_item_id?: string | null;
}>;
id?: UUID
tipo: 'BASICA' | 'COMPLEMENTARIA'
cita: string
tipo_fuente?: 'MANUAL' | 'BIBLIOTECA'
biblioteca_item_id?: string | null
}>
export async function subjects_update_bibliografia(
subjectId: UUID,
entries: BibliografiaUpsertInput
entries: BibliografiaUpsertInput,
): 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 = {
archivoId: UUID;
signedUrl: string;
@@ -183,10 +209,18 @@ export async function subjects_update_bibliografia(
nombre?: string;
}; */
export async function subjects_generate_document(subjectId: UUID): Promise<DocumentoResult> {
return invokeEdge<DocumentoResult>(EDGE.subjects_generate_document, { subjectId });
export async function subjects_generate_document(
subjectId: UUID,
): Promise<DocumentoResult> {
return invokeEdge<DocumentoResult>(EDGE.subjects_generate_document, {
subjectId,
})
}
export async function subjects_get_document(subjectId: UUID): Promise<DocumentoResult | null> {
return invokeEdge<DocumentoResult | null>(EDGE.subjects_get_document, { subjectId });
export async function subjects_get_document(
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()
return useMutation({
mutationFn: (payload: { planId: UUID; jsonMateria: any }) =>
mutationFn: (payload: { planId: UUID; jsonAsignatura: any }) =>
subjects_persist_from_ai(payload),
onSuccess: (subject) => {
qc.setQueryData(qk.asignatura(subject.id), subject)

View File

@@ -1,14 +1,14 @@
import type {
Materia,
MateriaStructure,
UnidadTematica,
BibliografiaEntry,
CambioMateria,
DocumentoMateria,
LibraryResource
} from '@/types/materia';
import type {
Asignatura,
AsignaturaStructure,
UnidadTematica,
BibliografiaEntry,
CambioAsignatura,
DocumentoAsignatura,
LibraryResource,
} from '@/types/asignatura'
export const mockMateria: Materia = {
export const mockAsignatura: Asignatura = {
id: '1',
nombre: 'Inteligencia Artificial Aplicada',
clave: 'IAA-401',
@@ -20,9 +20,9 @@ export const mockMateria: Materia = {
carrera: 'Ingeniería en Sistemas Computacionales',
facultad: 'Facultad de Ingeniería',
estructuraId: 'estructura-1',
};
}
export const mockEstructura: MateriaStructure = {
export const mockEstructura: AsignaturaStructure = {
id: 'estructura-1',
nombre: 'Plantilla SEP Licenciatura',
campos: [
@@ -31,7 +31,7 @@ export const mockEstructura: MateriaStructure = {
nombre: 'Objetivo General',
tipo: 'texto_largo',
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...',
},
{
@@ -46,14 +46,14 @@ export const mockEstructura: MateriaStructure = {
nombre: 'Justificación',
tipo: 'texto_largo',
obligatorio: true,
descripcion: 'Relevancia de la materia en el plan de estudios',
descripcion: 'Relevancia de la asignatura en el plan de estudios',
},
{
id: 'requisitos',
nombre: 'Requisitos / Seriación',
tipo: 'texto',
obligatorio: false,
descripcion: 'Materias previas requeridas',
descripcion: 'Asignaturas previas requeridas',
},
{
id: 'estrategias_didacticas',
@@ -77,27 +77,49 @@ export const mockEstructura: MateriaStructure = {
descripcion: 'Características requeridas del profesor',
},
],
};
}
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.',
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',
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.',
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.',
};
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.',
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',
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 asignatura proporciona las competencias necesarias para que el egresado pueda innovar y contribuir al desarrollo tecnológico del país.',
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',
nombre: 'Fundamentos de Inteligencia Artificial',
numero: 1,
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-3', nombre: 'Ética en IA', descripcion: 'Consideraciones éticas y responsabilidad', horasEstimadas: 2 },
{
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-3',
nombre: 'Ética en IA',
descripcion: 'Consideraciones éticas y responsabilidad',
horasEstimadas: 2,
},
],
},
{
@@ -105,9 +127,24 @@ export const mockContenidoTematico: UnidadTematica[] = [
nombre: 'Machine Learning',
numero: 2,
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-3', nombre: 'Evaluación de modelos', descripcion: 'Métricas y validación cruzada', horasEstimadas: 4 },
{
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-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',
numero: 3,
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-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 },
{
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-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',
numero: 4,
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-3', nombre: 'Sistemas de recomendación', descripcion: 'Filtrado colaborativo y contenido', horasEstimadas: 4 },
{
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-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',
tipo: 'BASICA',
@@ -153,13 +225,14 @@ export const mockBibliografia: BibliografiaEntry[] = [
{
id: 'bib-2',
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',
fuenteBiblioteca: {
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',
editorial: 'O\'Reilly Media',
editorial: "O'Reilly Media",
anio: 2022,
isbn: '978-1098125974',
tipo: 'libro',
@@ -187,9 +260,9 @@ export const mockBibliografia: BibliografiaEntry[] = [
disponible: false,
},
},
];
]
export const mockHistorial: CambioMateria[] = [
export const mockHistorial: Array<CambioAsignatura> = [
{
id: 'cambio-1',
tipo: 'datos',
@@ -228,17 +301,17 @@ export const mockHistorial: CambioMateria[] = [
usuario: 'Sistema',
fecha: new Date('2024-12-06T11:30:00'),
},
];
]
export const mockDocumentoSep: DocumentoMateria = {
export const mockDocumentoSep: DocumentoAsignatura = {
id: 'doc-1',
materiaId: '1',
asignaturaId: '1',
version: 3,
fechaGeneracion: new Date('2024-12-06T11:30:00'),
estado: 'listo',
};
}
export const mockLibraryResources: LibraryResource[] = [
export const mockLibraryResources: Array<LibraryResource> = [
{
id: 'lib-1',
titulo: 'Artificial Intelligence: A Modern Approach',
@@ -251,9 +324,10 @@ export const mockLibraryResources: LibraryResource[] = [
},
{
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',
editorial: 'O\'Reilly Media',
editorial: "O'Reilly Media",
anio: 2022,
isbn: '978-1098125974',
tipo: 'libro',
@@ -299,4 +373,4 @@ export const mockLibraryResources: LibraryResource[] = [
tipo: 'libro',
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 = [
{ id: "ing", nombre: "Facultad de Ingeniería" },
{ id: 'ing', nombre: 'Facultad de Ingeniería' },
{
id: "med",
nombre: "Facultad de Medicina en medicina en medicina en medicina",
id: 'med',
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 = [
{ id: "sis", nombre: "Ing. en Sistemas", facultadId: "ing" },
{ id: "ind", nombre: "Ing. Industrial", facultadId: "ing" },
{ id: "medico", nombre: "Médico Cirujano", facultadId: "med" },
{ id: "act", nombre: "Actuaría", facultadId: "neg" },
];
{ id: 'sis', nombre: 'Ing. en Sistemas', facultadId: 'ing' },
{ id: 'ind', nombre: 'Ing. Industrial', facultadId: 'ing' },
{ id: 'medico', nombre: 'Médico Cirujano', facultadId: 'med' },
{ id: 'act', nombre: 'Actuaría', facultadId: 'neg' },
]
export const NIVELES: Array<NivelPlanEstudio> = [
"Licenciatura",
"Maestría",
"Doctorado",
"Especialidad",
"Diplomado",
"Otro",
];
'Licenciatura',
'Maestría',
'Doctorado',
'Especialidad',
'Diplomado',
'Otro',
]
export const TIPOS_CICLO: Array<TipoCiclo> = [
"Semestre",
"Cuatrimestre",
"Trimestre",
"Otro",
];
'Semestre',
'Cuatrimestre',
'Trimestre',
'Otro',
]
export const PLANES_EXISTENTES = [
{
id: "plan-2021-sis",
nombre: "ISC 2021",
estado: "Aprobado",
id: 'plan-2021-sis',
nombre: 'ISC 2021',
estado: 'Aprobado',
anio: 2021,
facultadId: "ing",
carreraId: "sis",
facultadId: 'ing',
carreraId: 'sis',
},
{
id: "plan-2020-ind",
nombre: "I. Industrial 2020",
estado: "Aprobado",
id: 'plan-2020-ind',
nombre: 'I. Industrial 2020',
estado: 'Aprobado',
anio: 2020,
facultadId: "ing",
carreraId: "ind",
facultadId: 'ing',
carreraId: 'ind',
},
{
id: "plan-2019-med",
nombre: "Medicina 2019",
estado: "Vigente",
id: 'plan-2019-med',
nombre: 'Medicina 2019',
estado: 'Vigente',
anio: 2019,
facultadId: "med",
carreraId: "medico",
facultadId: 'med',
carreraId: 'medico',
},
];
]
export const ARCHIVOS = [
{
id: "file-1",
nombre: "Sílabo POO 2023.docx",
tipo: "docx",
tamaño: "245 KB",
id: 'file-1',
nombre: 'Sílabo POO 2023.docx',
tipo: 'docx',
tamaño: '245 KB',
},
{
id: "file-2",
nombre: "Guía de prácticas BD.pdf",
tipo: "pdf",
tamaño: "1.2 MB",
id: 'file-2',
nombre: 'Guía de prácticas BD.pdf',
tipo: 'pdf',
tamaño: '1.2 MB',
},
{
id: "file-3",
nombre: "Rúbrica evaluación proyectos.xlsx",
tipo: "xlsx",
tamaño: "89 KB",
id: 'file-3',
nombre: 'Rúbrica evaluación proyectos.xlsx',
tipo: 'xlsx',
tamaño: '89 KB',
},
{
id: "file-4",
nombre: "Banco de reactivos IA.docx",
tipo: "docx",
tamaño: "567 KB",
id: 'file-4',
nombre: 'Banco de reactivos IA.docx',
tipo: 'docx',
tamaño: '567 KB',
},
{
id: "file-5",
nombre: "Material didáctico Web.pdf",
tipo: "pdf",
tamaño: "3.4 MB",
id: 'file-5',
nombre: 'Asignatural didáctico Web.pdf',
tipo: 'pdf',
tamaño: '3.4 MB',
},
];
]
export const REPOSITORIOS = [
{
id: "repo-1",
nombre: "Materiales ISC 2024",
descripcion: "Documentos de referencia para Ingeniería en Sistemas",
id: 'repo-1',
nombre: 'Asignaturales ISC 2024',
descripcion: 'Documentos de referencia para Ingeniería en Sistemas',
cantidadArchivos: 45,
},
{
id: "repo-2",
nombre: "Lineamientos SEP",
descripcion: "Documentos oficiales y normativas SEP actualizadas",
id: 'repo-2',
nombre: 'Lineamientos SEP',
descripcion: 'Documentos oficiales y normativas SEP actualizadas',
cantidadArchivos: 12,
},
{
id: "repo-3",
nombre: "Bibliografía Digital",
descripcion: "Recursos bibliográficos digitalizados",
id: 'repo-3',
nombre: 'Bibliografía Digital',
descripcion: 'Recursos bibliográficos digitalizados',
cantidadArchivos: 128,
},
{
id: "repo-4",
nombre: "Plantillas Institucionales",
descripcion: "Formatos y plantillas oficiales ULSA",
id: 'repo-4',
nombre: 'Plantillas Institucionales',
descripcion: 'Formatos y plantillas oficiales ULSA',
cantidadArchivos: 23,
},
];
]
export const PLANTILLAS_ANEXO_1 = [
{
id: "sep-2025",
name: "Licenciatura RVOE SEP.docx",
versions: ["v2025.2 (Vigente)", "v2025.1", "v2024.Final"],
id: 'sep-2025',
name: 'Licenciatura RVOE SEP.docx',
versions: ['v2025.2 (Vigente)', 'v2025.1', 'v2024.Final'],
},
{
id: "interno-mix",
name: "Estándar Institucional Mixto.docx",
versions: ["v2.0", "v1.5", "v1.0-beta"],
id: 'interno-mix',
name: 'Estándar Institucional Mixto.docx',
versions: ['v2.0', 'v1.5', 'v1.0-beta'],
},
{
id: "conacyt",
name: "Formato Posgrado CONAHCYT.docx",
versions: ["v3.0 (2025)", "v2.8"],
id: 'conacyt',
name: 'Formato Posgrado CONAHCYT.docx',
versions: ['v3.0 (2025)', 'v2.8'],
},
];
]
export const PLANTILLAS_ANEXO_2 = [
{
id: "sep-2017-xlsx",
name: "Licenciatura RVOE 2017.xlsx",
versions: ["v2017.0", "v2018.1", "v2019.2", "v2020.Final"],
id: 'sep-2017-xlsx',
name: 'Licenciatura RVOE 2017.xlsx',
versions: ['v2017.0', 'v2018.1', 'v2019.2', 'v2020.Final'],
},
{
id: "interno-mix-xlsx",
name: "Estándar Institucional Mixto.xlsx",
versions: ["v1.0", "v1.5"],
id: 'interno-mix-xlsx',
name: 'Estándar Institucional Mixto.xlsx',
versions: ['v1.0', 'v1.5'],
},
{
id: "conacyt-xlsx",
name: "Formato Posgrado CONAHCYT.xlsx",
versions: ["v1.0", "v2.0"],
id: 'conacyt-xlsx',
name: 'Formato Posgrado CONAHCYT.xlsx',
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'
import { useState, useMemo } from 'react'
import type { Materia } from '@/types/plan'
import type { Asignatura } from '@/types/plan'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
@@ -46,7 +46,7 @@ const tipoConfig: Record<string, { label: string; className: string }> = {
}
// --- Mapeadores de API ---
const mapAsignaturas = (asigApi: Array<any> = []): Array<Materia> => {
const mapAsignaturas = (asigApi: Array<any> = []): Array<Asignatura> => {
return asigApi.map((asig) => ({
id: asig.id,
clave: asig.codigo,
@@ -63,10 +63,10 @@ const mapAsignaturas = (asigApi: Array<any> = []): Array<Materia> => {
}
export const Route = createFileRoute('/planes/$planId/_detalle/asignaturas')({
component: MateriasPage,
component: AsignaturasPage,
})
function MateriasPage() {
function AsignaturasPage() {
const { planId } = Route.useParams()
const navigate = useNavigate()
@@ -82,13 +82,13 @@ function MateriasPage() {
const [filterLinea, setFilterLinea] = useState<string>('all')
// 3. Procesamiento de datos
const materias = useMemo(
const asignaturas = useMemo(
() => mapAsignaturas(asignaturasApi),
[asignaturasApi],
)
const lineas = useMemo(() => lineasApi || [], [lineasApi])
const filteredMaterias = materias.filter((m) => {
const filteredAsignaturas = asignaturas.filter((m) => {
const matchesSearch =
m.nombre.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>
<h2 className="text-foreground text-xl font-bold">
Materias del Plan
Asignaturas del Plan
</h2>
<p className="text-muted-foreground mt-1 text-sm">
{materias.length} materias en total {filteredMaterias.length}{' '}
filtradas
{asignaturas.length} asignaturas en total {' '}
{filteredAsignaturas.length} filtradas
</p>
</div>
@@ -132,7 +132,7 @@ function MateriasPage() {
<Copy className="mr-2 h-4 w-4" /> Clonar
</Button>
<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>
</div>
</div>
@@ -207,12 +207,12 @@ function MateriasPage() {
</TableRow>
</TableHeader>
<TableBody>
{filteredMaterias.length === 0 ? (
{filteredAsignaturas.length === 0 ? (
<TableRow>
<TableCell colSpan={8} className="h-40 text-center">
<div className="text-muted-foreground flex flex-col items-center justify-center">
<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">
Intenta cambiar los filtros de búsqueda
</p>
@@ -220,59 +220,59 @@ function MateriasPage() {
</TableCell>
</TableRow>
) : (
filteredMaterias.map((materia) => (
filteredAsignaturas.map((asignatura) => (
<TableRow
key={materia.id}
key={asignatura.id}
className="group cursor-pointer transition-colors hover:bg-slate-50/80"
onClick={() =>
navigate({
to: '/planes/$planId/asignaturas/$asignaturaId',
params: {
planId,
asignaturaId: materia.id, // 👈 puede ser índice, consecutivo o slug
asignaturaId: asignatura.id, // 👈 puede ser índice, consecutivo o slug
},
state: {
realId: materia.id, // 👈 ID largo oculto
asignaturaId: materia.id,
realId: asignatura.id, // 👈 ID largo oculto
asignaturaId: asignatura.id,
} as any,
})
}
>
<TableCell className="font-mono text-xs font-bold text-slate-400">
{materia.clave}
{asignatura.clave}
</TableCell>
<TableCell className="font-semibold text-slate-700">
{materia.nombre}
{asignatura.nombre}
</TableCell>
<TableCell className="text-center font-medium">
{materia.creditos}
{asignatura.creditos}
</TableCell>
<TableCell className="text-center">
{materia.ciclo ? (
{asignatura.ciclo ? (
<Badge variant="outline" className="font-normal">
Ciclo {materia.ciclo}
Ciclo {asignatura.ciclo}
</Badge>
) : (
<span className="text-slate-300"></span>
)}
</TableCell>
<TableCell className="text-sm text-slate-600">
{getLineaNombre(materia.lineaCurricularId)}
{getLineaNombre(asignatura.lineaCurricularId)}
</TableCell>
<TableCell>
<Badge
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>
</TableCell>
<TableCell>
<Badge
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>
</TableCell>
<TableCell>

View File

@@ -9,7 +9,7 @@ import {
} from 'lucide-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 { 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) => ({
id: asig.id,
clave: asig.codigo,
@@ -104,13 +106,13 @@ function StatItem({
)
}
function MateriaCardItem({
materia,
function AsignaturaCardItem({
asignatura,
onDragStart,
isDragging,
onClick,
}: {
materia: Materia
asignatura: Asignatura
onDragStart: (e: React.DragEvent, id: string) => void
isDragging: boolean
onClick: () => void
@@ -118,7 +120,7 @@ function MateriaCardItem({
return (
<button
draggable
onDragStart={(e) => onDragStart(e, materia.id)}
onDragStart={(e) => onDragStart(e, asignatura.id)}
onClick={onClick}
className={`group cursor-grab rounded-lg border bg-white p-3 shadow-sm transition-all active:cursor-grabbing ${
isDragging
@@ -128,21 +130,21 @@ function MateriaCardItem({
>
<div className="mb-1 flex items-start justify-between">
<span className="font-mono text-[10px] font-bold text-slate-400">
{materia.clave}
{asignatura.clave}
</span>
<Badge
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>
</div>
<p className="mb-1 text-xs leading-tight font-bold text-slate-700">
{materia.nombre}
{asignatura.nombre}
</p>
<div className="mt-2 flex items-center justify-between">
<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>
<GripVertical
size={12}
@@ -166,11 +168,14 @@ function MapaCurricularPage() {
const { data: lineasApi, isLoading: loadingLineas } = usePlanLineas(planId)
// 2. Estado Local (Para interactividad)
const [materias, setMaterias] = useState<Array<Materia>>([])
const [asignaturas, setAsignaturas] = useState<Array<Asignatura>>([])
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 [selectedMateria, setSelectedMateria] = useState<Materia | null>(null)
const [selectedAsignatura, setSelectedAsignatura] =
useState<Asignatura | null>(null)
const [hasAreaComun, setHasAreaComun] = useState(false)
const [nombreNuevaLinea, setNombreNuevaLinea] = useState('') // Para el input de nombre personalizado
@@ -236,7 +241,8 @@ function MapaCurricularPage() {
// 3. Sincronizar API -> Estado Local
useEffect(() => {
if (asignaturasApi) setMaterias(mapAsignaturasToMaterias(asignaturasApi))
if (asignaturasApi)
setAsignaturas(mapAsignaturasToAsignaturas(asignaturasApi))
}, [asignaturasApi])
useEffect(() => {
@@ -247,23 +253,23 @@ function MapaCurricularPage() {
const ciclosArray = Array.from({ length: ciclosTotales }, (_, i) => i + 1)
// 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
const handleSaveChanges = () => {
if (!editingData) return
console.log(materias)
console.log(asignaturas)
setMaterias((prev) =>
setAsignaturas((prev) =>
prev.map((m) => (m.id === editingData.id ? { ...editingData } : m)),
)
setIsEditModalOpen(false)
}
// 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.
const unassignedMaterias = materias.filter((m) => m.ciclo === null)
const unassignedAsignaturas = asignaturas.filter((m) => m.ciclo === null)
// --- Lógica de Gestión ---
const agregarLinea = (nombre: string) => {
@@ -272,7 +278,7 @@ function MapaCurricularPage() {
}
const borrarLinea = (id: string) => {
setMaterias((prev) =>
setAsignaturas((prev) =>
prev.map((m) =>
m.lineaCurricularId === id
? { ...m, ciclo: null, lineaCurricularId: null }
@@ -284,7 +290,7 @@ function MapaCurricularPage() {
// --- Selectores/Cálculos ---
const getTotalesCiclo = (ciclo: number) => {
return materias
return asignaturas
.filter((m) => m.ciclo === ciclo)
.reduce(
(acc, m) => ({
@@ -297,7 +303,7 @@ function MapaCurricularPage() {
}
const getSubtotalLinea = (lineaId: string) => {
return materias
return asignaturas
.filter((m) => m.lineaCurricularId === lineaId && m.ciclo !== null)
.reduce(
(acc, m) => ({
@@ -310,7 +316,7 @@ function MapaCurricularPage() {
}
const handleDragStart = (e: React.DragEvent, id: string) => {
setDraggedMateria(id)
setDraggedAsignatura(id)
e.dataTransfer.effectAllowed = 'move'
}
const handleDragOver = (e: React.DragEvent) => e.preventDefault()
@@ -320,21 +326,21 @@ function MapaCurricularPage() {
lineaId: string | null,
) => {
e.preventDefault()
if (draggedMateria) {
setMaterias((prev) =>
if (draggedAsignatura) {
setAsignaturas((prev) =>
prev.map((m) =>
m.id === draggedMateria
m.id === draggedAsignatura
? { ...m, ciclo, lineaCurricularId: lineaId }
: m,
),
)
setDraggedMateria(null)
setDraggedAsignatura(null)
}
}
const stats = useMemo(
() =>
materias.reduce(
asignaturas.reduce(
(acc, m) => {
if (m.ciclo !== null) {
acc.cr += m.creditos || 0
@@ -345,7 +351,7 @@ function MapaCurricularPage() {
},
{ cr: 0, hd: 0, hi: 0 },
),
[materias],
[asignaturas],
)
if (loadingAsig || loadingLineas)
@@ -358,14 +364,14 @@ function MapaCurricularPage() {
<div>
<h2 className="text-xl font-bold">Mapa Curricular</h2>
<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>
</div>
<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">
<AlertTriangle size={14} className="mr-1" />{' '}
{materias.filter((m) => !m.ciclo).length} sin asignar
{asignaturas.filter((m) => !m.ciclo).length} sin asignar
</Badge>
)}
<DropdownMenu>
@@ -474,16 +480,16 @@ function MapaCurricularPage() {
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"
>
{materias
{asignaturas
.filter(
(m) =>
m.ciclo === ciclo && m.lineaCurricularId === linea.id,
)
.map((m) => (
<MateriaCardItem
<AsignaturaCardItem
key={m.id}
materia={m}
isDragging={draggedMateria === m.id}
asignatura={m}
isDragging={draggedAsignatura === m.id}
onDragStart={handleDragStart}
onClick={() => {
setEditingData(m)
@@ -534,35 +540,35 @@ function MapaCurricularPage() {
</div>
</div>
{/* Materias Sin Asignar */}
{/* Asignaturas Sin Asignar */}
{/* 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="mb-4 flex items-center justify-between">
<div className="flex items-center gap-2 text-slate-600">
<h3 className="text-sm font-bold tracking-wider uppercase">
Bandeja de Entrada / Materias sin asignar
Bandeja de Entrada / Asignaturas sin asignar
</h3>
<Badge variant="secondary">{unassignedMaterias.length}</Badge>
<Badge variant="secondary">{unassignedAsignaturas.length}</Badge>
</div>
<p className="text-xs text-slate-400">
Arrastra una materia aquí para quitarla del mapa
Arrastra una asignatura aquí para quitarla del mapa
</p>
</div>
<div
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-slate-200 bg-white/50'
}`}
onDragOver={handleDragOver}
onDrop={(e) => handleDrop(e, null, null)} // Limpia ciclo y línea
>
{unassignedMaterias.map((m) => (
{unassignedAsignaturas.map((m) => (
<div key={m.id} className="w-[200px]">
<MateriaCardItem
materia={m}
isDragging={draggedMateria === m.id}
<AsignaturaCardItem
asignatura={m}
isDragging={draggedAsignatura === m.id}
onDragStart={handleDragStart}
onClick={() => {
setEditingData(m) // Cargamos los datos en el estado de edición
@@ -571,9 +577,9 @@ function MapaCurricularPage() {
/>
</div>
))}
{unassignedMaterias.length === 0 && (
{unassignedAsignaturas.length === 0 && (
<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.
</div>
)}
@@ -585,7 +591,7 @@ function MapaCurricularPage() {
<DialogContent className="sm:max-w-[550px]">
<DialogHeader>
<DialogTitle className="font-bold text-slate-700">
Editar Materia
Editar Asignatura
</DialogTitle>
</DialogHeader>
@@ -735,10 +741,10 @@ function MapaCurricularPage() {
</label>
<Select>
<SelectTrigger>
<SelectValue placeholder="Seleccionar materia..." />
<SelectValue placeholder="Seleccionar asignatura..." />
</SelectTrigger>
<SelectContent>
{materias.map((m) => (
{asignaturas.map((m) => (
<SelectItem key={m.id} value={m.clave}>
{m.nombre}
</SelectItem>

View File

@@ -223,7 +223,7 @@ function RouteComponent() {
Mapa Curricular
</Tab>
<Tab to="/planes/$planId/asignaturas" params={{ planId }}>
Materias
Asignaturas
</Tab>
<Tab to="/planes/$planId/flujo" params={{ planId }}>
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'
export const Route = createFileRoute(
'/planes/$planId/asignaturas/$asignaturaId'
'/planes/$planId/asignaturas/$asignaturaId',
)({
component: RouteComponent,
})
@@ -12,7 +12,7 @@ function RouteComponent() {
return (
<div>
<MateriaDetailPage></MateriaDetailPage>
<AsignaturaDetailPage></AsignaturaDetailPage>
</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'
| '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 {
id: string
@@ -36,15 +36,15 @@ export interface LineaCurricular {
color?: string
}
export interface Materia {
export interface Asignatura {
id: string
clave: string
nombre: string
creditos: number
ciclo: number | null
lineaCurricularId: string | null
tipo: TipoMateria
estado: MateriaStatus
tipo: TipoAsignatura
estado: AsignaturaStatus
orden?: number
hd: number // <--- Añadir
hi: number // <--- Añadir
@@ -103,7 +103,7 @@ export interface DocumentoPlan {
export type PlanTab =
| 'datos-generales'
| 'mapa-curricular'
| 'materias'
| 'asignaturas'
| 'flujo'
| 'ia'
| 'documento'