Compare commits
2 Commits
2c702d7d67
...
2185901c7a
| Author | SHA1 | Date | |
|---|---|---|---|
| 2185901c7a | |||
| d0b05256b0 |
3
bun.lock
3
bun.lock
@@ -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=="],
|
||||||
|
|||||||
@@ -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}
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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) => (
|
||||||
@@ -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 }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
];
|
]
|
||||||
@@ -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'],
|
||||||
},
|
},
|
||||||
];
|
]
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
119
src/types/asignatura.ts
Normal 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
|
||||||
|
}
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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'
|
||||||
|
|||||||
Reference in New Issue
Block a user