Compare commits
2 Commits
9584cd0c04
...
cbe4e54309
| Author | SHA1 | Date | |
|---|---|---|---|
| cbe4e54309 | |||
| 3f0d2368bc |
@@ -21,7 +21,6 @@
|
|||||||
"@radix-ui/react-avatar": "^1.1.11",
|
"@radix-ui/react-avatar": "^1.1.11",
|
||||||
"@radix-ui/react-checkbox": "^1.3.3",
|
"@radix-ui/react-checkbox": "^1.3.3",
|
||||||
"@radix-ui/react-collapsible": "^1.1.12",
|
"@radix-ui/react-collapsible": "^1.1.12",
|
||||||
"@radix-ui/react-context-menu": "^2.2.16",
|
|
||||||
"@radix-ui/react-dialog": "^1.1.15",
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||||
"@radix-ui/react-label": "^2.1.8",
|
"@radix-ui/react-label": "^2.1.8",
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { useEffect, useState } from 'react'
|
||||||
import {
|
import {
|
||||||
Plus,
|
Plus,
|
||||||
Search,
|
Search,
|
||||||
@@ -7,8 +8,25 @@ import {
|
|||||||
Edit3,
|
Edit3,
|
||||||
Save,
|
Save,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { useEffect, useState } from 'react'
|
import { Card, CardContent } from '@/components/ui/card'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Input } from '@/components/ui/input'
|
||||||
|
import { Textarea } from '@/components/ui/textarea'
|
||||||
|
import { Badge } from '@/components/ui/badge'
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from '@/components/ui/dialog'
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@/components/ui/select'
|
||||||
import {
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
AlertDialogAction,
|
AlertDialogAction,
|
||||||
@@ -19,29 +37,10 @@ import {
|
|||||||
AlertDialogHeader,
|
AlertDialogHeader,
|
||||||
AlertDialogTitle,
|
AlertDialogTitle,
|
||||||
} from '@/components/ui/alert-dialog'
|
} from '@/components/ui/alert-dialog'
|
||||||
import { Badge } from '@/components/ui/badge'
|
|
||||||
import { Button } from '@/components/ui/button'
|
|
||||||
import { Card, CardContent } from '@/components/ui/card'
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
DialogTrigger,
|
|
||||||
} from '@/components/ui/dialog'
|
|
||||||
import { Input } from '@/components/ui/input'
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from '@/components/ui/select'
|
|
||||||
import { Textarea } from '@/components/ui/textarea'
|
|
||||||
import { useSubjectBibliografia } from '@/data/hooks/useSubjects'
|
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
// import { toast } from 'sonner';
|
import { useSubjectBibliografia } from '@/data/hooks/useSubjects'
|
||||||
// import { mockLibraryResources } from '@/data/mockMateriaData';
|
//import { toast } from 'sonner';
|
||||||
|
//import { mockLibraryResources } from '@/data/mockMateriaData';
|
||||||
|
|
||||||
export const mockLibraryResources = [
|
export const mockLibraryResources = [
|
||||||
{
|
{
|
||||||
@@ -85,23 +84,19 @@ export interface BibliografiaEntry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface BibliografiaTabProps {
|
interface BibliografiaTabProps {
|
||||||
id: string
|
bibliografia: BibliografiaEntry[]
|
||||||
bibliografia: Array<BibliografiaEntry>
|
onSave: (bibliografia: BibliografiaEntry[]) => void
|
||||||
onSave: (bibliografia: Array<BibliografiaEntry>) => void
|
|
||||||
isSaving: boolean
|
isSaving: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export function BibliographyItem({
|
export function BibliographyItem({
|
||||||
bibliografia,
|
bibliografia,
|
||||||
id,
|
|
||||||
onSave,
|
onSave,
|
||||||
isSaving,
|
isSaving,
|
||||||
}: BibliografiaTabProps) {
|
}: BibliografiaTabProps) {
|
||||||
console.log(id)
|
|
||||||
|
|
||||||
const { data: bibliografia2, isLoading: loadinmateria } =
|
const { data: bibliografia2, isLoading: loadinmateria } =
|
||||||
useSubjectBibliografia(id)
|
useSubjectBibliografia('9d4dda6a-488f-428a-8a07-38081592a641')
|
||||||
const [entries, setEntries] = useState<Array<BibliografiaEntry>>(bibliografia)
|
const [entries, setEntries] = useState<BibliografiaEntry[]>(bibliografia)
|
||||||
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false)
|
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false)
|
||||||
const [isLibraryDialogOpen, setIsLibraryDialogOpen] = useState(false)
|
const [isLibraryDialogOpen, setIsLibraryDialogOpen] = useState(false)
|
||||||
const [deleteId, setDeleteId] = useState<string | null>(null)
|
const [deleteId, setDeleteId] = useState<string | null>(null)
|
||||||
@@ -133,7 +128,7 @@ export function BibliographyItem({
|
|||||||
}
|
}
|
||||||
setEntries([...entries, newEntry])
|
setEntries([...entries, newEntry])
|
||||||
setIsAddDialogOpen(false)
|
setIsAddDialogOpen(false)
|
||||||
// toast.success('Referencia manual añadida');
|
//toast.success('Referencia manual añadida');
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleAddFromLibrary = (
|
const handleAddFromLibrary = (
|
||||||
@@ -150,7 +145,7 @@ export function BibliographyItem({
|
|||||||
}
|
}
|
||||||
setEntries([...entries, newEntry])
|
setEntries([...entries, newEntry])
|
||||||
setIsLibraryDialogOpen(false)
|
setIsLibraryDialogOpen(false)
|
||||||
// toast.success('Añadido desde biblioteca');
|
//toast.success('Añadido desde biblioteca');
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleUpdateCita = (id: string, cita: string) => {
|
const handleUpdateCita = (id: string, cita: string) => {
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { format, parseISO } from 'date-fns'
|
import { useState, useMemo } from 'react'
|
||||||
import { es } from 'date-fns/locale'
|
|
||||||
import {
|
import {
|
||||||
History,
|
History,
|
||||||
FileText,
|
FileText,
|
||||||
@@ -7,30 +6,31 @@ import {
|
|||||||
BookMarked,
|
BookMarked,
|
||||||
Sparkles,
|
Sparkles,
|
||||||
FileCheck,
|
FileCheck,
|
||||||
|
User,
|
||||||
Filter,
|
Filter,
|
||||||
Calendar,
|
Calendar,
|
||||||
Loader2,
|
Loader2,
|
||||||
Eye,
|
Eye,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { useState, useMemo } from 'react'
|
|
||||||
|
|
||||||
import { Badge } from '@/components/ui/badge'
|
|
||||||
import { Button } from '@/components/ui/button'
|
|
||||||
import { Card, CardContent } from '@/components/ui/card'
|
import { Card, CardContent } from '@/components/ui/card'
|
||||||
import {
|
import { Button } from '@/components/ui/button'
|
||||||
Dialog,
|
import { Badge } from '@/components/ui/badge'
|
||||||
DialogContent,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from '@/components/ui/dialog'
|
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuCheckboxItem,
|
DropdownMenuCheckboxItem,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from '@/components/ui/dropdown-menu'
|
} from '@/components/ui/dropdown-menu'
|
||||||
import { useSubjectHistorial } from '@/data/hooks/useSubjects'
|
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
import { format, parseISO } from 'date-fns'
|
||||||
|
import { es } from 'date-fns/locale'
|
||||||
|
import { useSubjectHistorial } from '@/data/hooks/useSubjects'
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from '@/components/ui/dialog'
|
||||||
|
|
||||||
const tipoConfig: Record<string, { label: string; icon: any; color: string }> =
|
const tipoConfig: Record<string, { label: string; icon: any; color: string }> =
|
||||||
{
|
{
|
||||||
@@ -53,9 +53,11 @@ const tipoConfig: Record<string, { label: string; icon: any; color: string }> =
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export function HistorialTab({ asignaturaId }) {
|
export function HistorialTab() {
|
||||||
// 1. Obtenemos los datos directamente dentro del componente
|
// 1. Obtenemos los datos directamente dentro del componente
|
||||||
const { data: rawData, isLoading } = useSubjectHistorial(asignaturaId)
|
const { data: rawData, isLoading } = useSubjectHistorial(
|
||||||
|
'9d4dda6a-488f-428a-8a07-38081592a641',
|
||||||
|
)
|
||||||
|
|
||||||
const [filtros, setFiltros] = useState<Set<string>>(
|
const [filtros, setFiltros] = useState<Set<string>>(
|
||||||
new Set(['datos', 'contenido', 'bibliografia', 'ia', 'documento']),
|
new Set(['datos', 'contenido', 'bibliografia', 'ia', 'documento']),
|
||||||
@@ -162,7 +164,7 @@ export function HistorialTab({ asignaturaId }) {
|
|||||||
groups[dateKey].push(cambio)
|
groups[dateKey].push(cambio)
|
||||||
return groups
|
return groups
|
||||||
},
|
},
|
||||||
{} as Record<string, Array<any>>,
|
{} as Record<string, any[]>,
|
||||||
)
|
)
|
||||||
|
|
||||||
const sortedDates = Object.keys(groupedHistorial).sort((a, b) =>
|
const sortedDates = Object.keys(groupedHistorial).sort((a, b) =>
|
||||||
|
|||||||
@@ -1,9 +1,4 @@
|
|||||||
import {
|
import { Link, useRouterState } from '@tanstack/react-router'
|
||||||
createFileRoute,
|
|
||||||
Link,
|
|
||||||
useParams,
|
|
||||||
useRouterState,
|
|
||||||
} from '@tanstack/react-router'
|
|
||||||
import { ArrowLeft, GraduationCap, Pencil, Sparkles } from 'lucide-react'
|
import { ArrowLeft, GraduationCap, Pencil, Sparkles } from 'lucide-react'
|
||||||
import { useCallback, useState, useEffect } from 'react'
|
import { useCallback, useState, useEffect } from 'react'
|
||||||
|
|
||||||
@@ -36,7 +31,6 @@ export interface BibliografiaEntry {
|
|||||||
fuenteBiblioteca?: any
|
fuenteBiblioteca?: any
|
||||||
}
|
}
|
||||||
export interface BibliografiaTabProps {
|
export interface BibliografiaTabProps {
|
||||||
id: string
|
|
||||||
bibliografia: Array<BibliografiaEntry>
|
bibliografia: Array<BibliografiaEntry>
|
||||||
onSave: (bibliografia: Array<BibliografiaEntry>) => void
|
onSave: (bibliografia: Array<BibliografiaEntry>) => void
|
||||||
isSaving: boolean
|
isSaving: boolean
|
||||||
@@ -59,34 +53,38 @@ function EditableHeaderField({
|
|||||||
onSave: (val: string) => void
|
onSave: (val: string) => void
|
||||||
className?: string
|
className?: string
|
||||||
}) {
|
}) {
|
||||||
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault()
|
||||||
|
;(e.currentTarget as HTMLElement).blur() // Quita el foco
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleBlur = (e: React.FocusEvent<HTMLElement>) => {
|
||||||
|
const newValue = e.currentTarget.textContent || ''
|
||||||
|
if (newValue !== value.toString()) {
|
||||||
|
onSave(newValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<input
|
<span
|
||||||
type="text"
|
contentEditable
|
||||||
value={String(value)}
|
suppressContentEditableWarning
|
||||||
onChange={(e) => onSave(e.target.value)}
|
onKeyDown={handleKeyDown}
|
||||||
onBlur={(e) => onSave(e.target.value)}
|
onBlur={handleBlur}
|
||||||
className={` w-[${String(value).length || 1}ch] max-w-[6ch] border-none bg-transparent text-center outline-none focus:ring-2 focus:ring-blue-400 ${className ?? ''} `}
|
className={`cursor-text rounded px-1 transition-all outline-none focus:ring-2 focus:ring-blue-400 ${className}`}
|
||||||
/>
|
>
|
||||||
|
{value}
|
||||||
|
</span>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Route = createFileRoute(
|
|
||||||
'/planes/$planId/asignaturas/$asignaturaId',
|
|
||||||
)({
|
|
||||||
component: MateriaDetailPage,
|
|
||||||
})
|
|
||||||
|
|
||||||
export default function MateriaDetailPage() {
|
export default function MateriaDetailPage() {
|
||||||
const routerState = useRouterState()
|
const routerState = useRouterState()
|
||||||
const state = routerState.location.state as any
|
const state = routerState.location.state as any
|
||||||
const { asignaturaId } = useParams({
|
const { data: asignaturasApi, isLoading: loadingAsig } = useSubject(
|
||||||
from: '/planes/$planId/asignaturas/$asignaturaId',
|
state?.realId,
|
||||||
})
|
)
|
||||||
const { planId } = useParams({
|
|
||||||
from: '/planes/$planId/asignaturas/$asignaturaId',
|
|
||||||
})
|
|
||||||
const { data: asignaturasApi, isLoading: loadingAsig } =
|
|
||||||
useSubject(asignaturaId)
|
|
||||||
// 1. Asegúrate de tener estos estados en tu componente principal
|
// 1. Asegúrate de tener estos estados en tu componente principal
|
||||||
const [messages, setMessages] = useState<Array<IAMessage>>([])
|
const [messages, setMessages] = useState<Array<IAMessage>>([])
|
||||||
const [datosGenerales, setDatosGenerales] = useState({})
|
const [datosGenerales, setDatosGenerales] = useState({})
|
||||||
@@ -104,10 +102,10 @@ export default function MateriaDetailPage() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (asignaturasApi) {
|
if (asignaturasApi) {
|
||||||
setHeaderData({
|
setHeaderData({
|
||||||
codigo: asignaturasApi.codigo ?? '',
|
codigo: asignaturasApi?.codigo ?? '',
|
||||||
nombre: asignaturasApi.nombre,
|
nombre: asignaturasApi?.nombre ?? '',
|
||||||
creditos: asignaturasApi.creditos,
|
creditos: asignaturasApi?.creditos ?? '',
|
||||||
ciclo: asignaturasApi.numero_ciclo ?? 0,
|
ciclo: asignaturasApi?.numero_ciclo ?? 0,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}, [asignaturasApi])
|
}, [asignaturasApi])
|
||||||
@@ -181,8 +179,7 @@ export default function MateriaDetailPage() {
|
|||||||
<section className="bg-gradient-to-b from-[#0b1d3a] to-[#0e2a5c] text-white">
|
<section className="bg-gradient-to-b from-[#0b1d3a] to-[#0e2a5c] text-white">
|
||||||
<div className="mx-auto max-w-7xl px-6 py-10">
|
<div className="mx-auto max-w-7xl px-6 py-10">
|
||||||
<Link
|
<Link
|
||||||
to="/planes/$planId"
|
to="/planes"
|
||||||
params={{ planId }}
|
|
||||||
className="mb-4 flex items-center gap-2 text-sm text-blue-200 hover:text-white"
|
className="mb-4 flex items-center gap-2 text-sm text-blue-200 hover:text-white"
|
||||||
>
|
>
|
||||||
<ArrowLeft className="h-4 w-4" /> Volver al plan
|
<ArrowLeft className="h-4 w-4" /> Volver al plan
|
||||||
@@ -227,14 +224,12 @@ export default function MateriaDetailPage() {
|
|||||||
<div className="flex flex-col items-end gap-2 text-right">
|
<div className="flex flex-col items-end gap-2 text-right">
|
||||||
{/* CRÉDITOS EDITABLES */}
|
{/* CRÉDITOS EDITABLES */}
|
||||||
<Badge variant="secondary" className="gap-1">
|
<Badge variant="secondary" className="gap-1">
|
||||||
<span className="inline-flex max-w-fit">
|
|
||||||
<EditableHeaderField
|
<EditableHeaderField
|
||||||
value={headerData.creditos}
|
value={headerData.creditos}
|
||||||
onSave={(val) =>
|
onSave={(val) =>
|
||||||
handleUpdateHeader('creditos', parseInt(val) || 0)
|
handleUpdateHeader('creditos', parseInt(val) || 0)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</span>
|
|
||||||
<span>créditos</span>
|
<span>créditos</span>
|
||||||
</Badge>
|
</Badge>
|
||||||
|
|
||||||
@@ -285,7 +280,6 @@ export default function MateriaDetailPage() {
|
|||||||
<TabsContent value="bibliografia">
|
<TabsContent value="bibliografia">
|
||||||
<BibliographyItem
|
<BibliographyItem
|
||||||
bibliografia={bibliografia}
|
bibliografia={bibliografia}
|
||||||
id={asignaturaId}
|
|
||||||
onSave={handleSaveBibliografia}
|
onSave={handleSaveBibliografia}
|
||||||
isSaving={isSaving}
|
isSaving={isSaving}
|
||||||
/>
|
/>
|
||||||
@@ -319,7 +313,7 @@ export default function MateriaDetailPage() {
|
|||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="historial">
|
<TabsContent value="historial">
|
||||||
<HistorialTab asignaturaId={asignaturaId} />
|
<HistorialTab />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,250 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"
|
|
||||||
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
function ContextMenu({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Root>) {
|
|
||||||
return <ContextMenuPrimitive.Root data-slot="context-menu" {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function ContextMenuTrigger({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Trigger>) {
|
|
||||||
return (
|
|
||||||
<ContextMenuPrimitive.Trigger data-slot="context-menu-trigger" {...props} />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ContextMenuGroup({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Group>) {
|
|
||||||
return (
|
|
||||||
<ContextMenuPrimitive.Group data-slot="context-menu-group" {...props} />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ContextMenuPortal({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Portal>) {
|
|
||||||
return (
|
|
||||||
<ContextMenuPrimitive.Portal data-slot="context-menu-portal" {...props} />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ContextMenuSub({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Sub>) {
|
|
||||||
return <ContextMenuPrimitive.Sub data-slot="context-menu-sub" {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function ContextMenuRadioGroup({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof ContextMenuPrimitive.RadioGroup>) {
|
|
||||||
return (
|
|
||||||
<ContextMenuPrimitive.RadioGroup
|
|
||||||
data-slot="context-menu-radio-group"
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ContextMenuSubTrigger({
|
|
||||||
className,
|
|
||||||
inset,
|
|
||||||
children,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof ContextMenuPrimitive.SubTrigger> & {
|
|
||||||
inset?: boolean
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<ContextMenuPrimitive.SubTrigger
|
|
||||||
data-slot="context-menu-sub-trigger"
|
|
||||||
data-inset={inset}
|
|
||||||
className={cn(
|
|
||||||
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
<ChevronRightIcon className="ml-auto" />
|
|
||||||
</ContextMenuPrimitive.SubTrigger>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ContextMenuSubContent({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof ContextMenuPrimitive.SubContent>) {
|
|
||||||
return (
|
|
||||||
<ContextMenuPrimitive.SubContent
|
|
||||||
data-slot="context-menu-sub-content"
|
|
||||||
className={cn(
|
|
||||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ContextMenuContent({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Content>) {
|
|
||||||
return (
|
|
||||||
<ContextMenuPrimitive.Portal>
|
|
||||||
<ContextMenuPrimitive.Content
|
|
||||||
data-slot="context-menu-content"
|
|
||||||
className={cn(
|
|
||||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-context-menu-content-available-height) min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
</ContextMenuPrimitive.Portal>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ContextMenuItem({
|
|
||||||
className,
|
|
||||||
inset,
|
|
||||||
variant = "default",
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Item> & {
|
|
||||||
inset?: boolean
|
|
||||||
variant?: "default" | "destructive"
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<ContextMenuPrimitive.Item
|
|
||||||
data-slot="context-menu-item"
|
|
||||||
data-inset={inset}
|
|
||||||
data-variant={variant}
|
|
||||||
className={cn(
|
|
||||||
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ContextMenuCheckboxItem({
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
checked,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof ContextMenuPrimitive.CheckboxItem>) {
|
|
||||||
return (
|
|
||||||
<ContextMenuPrimitive.CheckboxItem
|
|
||||||
data-slot="context-menu-checkbox-item"
|
|
||||||
className={cn(
|
|
||||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
checked={checked}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
|
||||||
<ContextMenuPrimitive.ItemIndicator>
|
|
||||||
<CheckIcon className="size-4" />
|
|
||||||
</ContextMenuPrimitive.ItemIndicator>
|
|
||||||
</span>
|
|
||||||
{children}
|
|
||||||
</ContextMenuPrimitive.CheckboxItem>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ContextMenuRadioItem({
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof ContextMenuPrimitive.RadioItem>) {
|
|
||||||
return (
|
|
||||||
<ContextMenuPrimitive.RadioItem
|
|
||||||
data-slot="context-menu-radio-item"
|
|
||||||
className={cn(
|
|
||||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
|
||||||
<ContextMenuPrimitive.ItemIndicator>
|
|
||||||
<CircleIcon className="size-2 fill-current" />
|
|
||||||
</ContextMenuPrimitive.ItemIndicator>
|
|
||||||
</span>
|
|
||||||
{children}
|
|
||||||
</ContextMenuPrimitive.RadioItem>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ContextMenuLabel({
|
|
||||||
className,
|
|
||||||
inset,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Label> & {
|
|
||||||
inset?: boolean
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<ContextMenuPrimitive.Label
|
|
||||||
data-slot="context-menu-label"
|
|
||||||
data-inset={inset}
|
|
||||||
className={cn(
|
|
||||||
"text-foreground px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ContextMenuSeparator({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Separator>) {
|
|
||||||
return (
|
|
||||||
<ContextMenuPrimitive.Separator
|
|
||||||
data-slot="context-menu-separator"
|
|
||||||
className={cn("bg-border -mx-1 my-1 h-px", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ContextMenuShortcut({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"span">) {
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
data-slot="context-menu-shortcut"
|
|
||||||
className={cn(
|
|
||||||
"text-muted-foreground ml-auto text-xs tracking-widest",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
ContextMenu,
|
|
||||||
ContextMenuTrigger,
|
|
||||||
ContextMenuContent,
|
|
||||||
ContextMenuItem,
|
|
||||||
ContextMenuCheckboxItem,
|
|
||||||
ContextMenuRadioItem,
|
|
||||||
ContextMenuLabel,
|
|
||||||
ContextMenuSeparator,
|
|
||||||
ContextMenuShortcut,
|
|
||||||
ContextMenuGroup,
|
|
||||||
ContextMenuPortal,
|
|
||||||
ContextMenuSub,
|
|
||||||
ContextMenuSubContent,
|
|
||||||
ContextMenuSubTrigger,
|
|
||||||
ContextMenuRadioGroup,
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="skeleton"
|
|
||||||
className={cn("bg-accent animate-pulse rounded-md", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Skeleton }
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
// document.api.ts
|
|
||||||
|
|
||||||
const DOCUMENT_PDF_URL =
|
|
||||||
'https://n8n.app.lci.ulsa.mx/webhook/62ca84ec-0adb-4006-aba1-32282d27d434'
|
|
||||||
|
|
||||||
interface GeneratePdfParams {
|
|
||||||
plan_estudio_id: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function fetchPlanPdf({
|
|
||||||
plan_estudio_id,
|
|
||||||
}: GeneratePdfParams): Promise<Blob> {
|
|
||||||
const response = await fetch(DOCUMENT_PDF_URL, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ plan_estudio_id }),
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error('Error al generar el PDF')
|
|
||||||
}
|
|
||||||
|
|
||||||
// n8n devuelve el archivo → lo tratamos como blob
|
|
||||||
return await response.blob()
|
|
||||||
}
|
|
||||||
@@ -25,7 +25,7 @@ const EDGE = {
|
|||||||
|
|
||||||
plans_import_from_files: 'plans_import_from_files',
|
plans_import_from_files: 'plans_import_from_files',
|
||||||
|
|
||||||
// plans_update_fields: 'plans_update_fields',
|
plans_update_fields: 'plans_update_fields',
|
||||||
plans_update_map: 'plans_update_map',
|
plans_update_map: 'plans_update_map',
|
||||||
plans_transition_state: 'plans_transition_state',
|
plans_transition_state: 'plans_transition_state',
|
||||||
|
|
||||||
@@ -349,26 +349,7 @@ export async function plans_update_fields(
|
|||||||
planId: UUID,
|
planId: UUID,
|
||||||
patch: PlansUpdateFieldsPatch,
|
patch: PlansUpdateFieldsPatch,
|
||||||
): Promise<PlanEstudio> {
|
): Promise<PlanEstudio> {
|
||||||
const supabase = supabaseBrowser()
|
return invokeEdge<PlanEstudio>(EDGE.plans_update_fields, { planId, patch })
|
||||||
|
|
||||||
const { data, error } = await supabase
|
|
||||||
.from('planes_estudio')
|
|
||||||
.update(patch)
|
|
||||||
.eq('id', planId)
|
|
||||||
.select(
|
|
||||||
`
|
|
||||||
*,
|
|
||||||
carreras (*, facultades(*)),
|
|
||||||
estructuras_plan (*),
|
|
||||||
estados_plan (*)
|
|
||||||
`,
|
|
||||||
)
|
|
||||||
.single()
|
|
||||||
|
|
||||||
throwIfError(error)
|
|
||||||
return requireData(data, 'No se pudo actualizar el plan.')
|
|
||||||
// Alternativa Edge Function:
|
|
||||||
// return invokeEdge<PlanEstudio>(EDGE.plans_update_fields, { planId, patch })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Operaciones del mapa curricular (mover/reordenar) */
|
/** Operaciones del mapa curricular (mover/reordenar) */
|
||||||
|
|||||||
@@ -169,10 +169,6 @@ export interface FileRoutesByTo {
|
|||||||
'/login': typeof LoginRoute
|
'/login': typeof LoginRoute
|
||||||
'/planes': typeof PlanesListaRouteRouteWithChildren
|
'/planes': typeof PlanesListaRouteRouteWithChildren
|
||||||
'/demo/tanstack-query': typeof DemoTanstackQueryRoute
|
'/demo/tanstack-query': typeof DemoTanstackQueryRoute
|
||||||
<<<<<<< HEAD
|
|
||||||
'/planes/PlanesListRoute': typeof PlanesPlanesListRouteRoute
|
|
||||||
=======
|
|
||||||
>>>>>>> 4950f7efbf664bbd31ac8a673fe594af5baf07f6
|
|
||||||
'/planes/$planId': typeof PlanesPlanIdIndexRoute
|
'/planes/$planId': typeof PlanesPlanIdIndexRoute
|
||||||
'/planes/nuevo': typeof PlanesListaNuevoRoute
|
'/planes/nuevo': typeof PlanesListaNuevoRoute
|
||||||
'/planes/$planId/asignaturas/$asignaturaId': typeof PlanesPlanIdAsignaturasAsignaturaIdRouteRoute
|
'/planes/$planId/asignaturas/$asignaturaId': typeof PlanesPlanIdAsignaturasAsignaturaIdRouteRoute
|
||||||
@@ -325,15 +321,7 @@ declare module '@tanstack/react-router' {
|
|||||||
'/planes/$planId/': {
|
'/planes/$planId/': {
|
||||||
id: '/planes/$planId/'
|
id: '/planes/$planId/'
|
||||||
path: '/planes/$planId'
|
path: '/planes/$planId'
|
||||||
<<<<<<< HEAD
|
|
||||||
<<<<<<< HEAD
|
|
||||||
fullPath: '/planes/$planId/'
|
fullPath: '/planes/$planId/'
|
||||||
=======
|
|
||||||
fullPath: '/planes/$planId'
|
|
||||||
>>>>>>> 4950f7efbf664bbd31ac8a673fe594af5baf07f6
|
|
||||||
=======
|
|
||||||
fullPath: '/planes/$planId/'
|
|
||||||
>>>>>>> cbe4e54 (Se cierran incidencias #10, #21, #24, #25; se añade generación manual de planes)
|
|
||||||
preLoaderRoute: typeof PlanesPlanIdIndexRouteImport
|
preLoaderRoute: typeof PlanesPlanIdIndexRouteImport
|
||||||
parentRoute: typeof rootRouteImport
|
parentRoute: typeof rootRouteImport
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ const formatLabel = (key: string) => {
|
|||||||
|
|
||||||
function DatosGeneralesPage() {
|
function DatosGeneralesPage() {
|
||||||
const { planId } = Route.useParams()
|
const { planId } = Route.useParams()
|
||||||
const { data, isLoading } = usePlan(planId)
|
const { data } = usePlan(planId)
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
// Inicializamos campos como un arreglo vacío
|
// Inicializamos campos como un arreglo vacío
|
||||||
const [campos, setCampos] = useState<Array<DatosGeneralesField>>([])
|
const [campos, setCampos] = useState<Array<DatosGeneralesField>>([])
|
||||||
@@ -89,7 +89,7 @@ function DatosGeneralesPage() {
|
|||||||
navigate({
|
navigate({
|
||||||
to: '/planes/$planId/iaplan',
|
to: '/planes/$planId/iaplan',
|
||||||
params: {
|
params: {
|
||||||
planId: planId, // o dinámico
|
planId: '1', // o dinámico
|
||||||
},
|
},
|
||||||
state: {
|
state: {
|
||||||
prefill: descripcion,
|
prefill: descripcion,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { createFileRoute, useParams } from '@tanstack/react-router'
|
import { createFileRoute } from '@tanstack/react-router'
|
||||||
import {
|
import {
|
||||||
FileText,
|
FileText,
|
||||||
Download,
|
Download,
|
||||||
@@ -6,52 +6,24 @@ import {
|
|||||||
ExternalLink,
|
ExternalLink,
|
||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
Clock,
|
Clock,
|
||||||
FileJson,
|
FileJson
|
||||||
} from 'lucide-react'
|
} from "lucide-react"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
import { Button } from '@/components/ui/button'
|
import { Card, CardContent } from "@/components/ui/card"
|
||||||
import { Card, CardContent } from '@/components/ui/card'
|
|
||||||
import { fetchPlanPdf } from '@/data/api/document.api'
|
|
||||||
|
|
||||||
export const Route = createFileRoute('/planes/$planId/_detalle/documento')({
|
export const Route = createFileRoute('/planes/$planId/_detalle/documento')({
|
||||||
component: RouteComponent,
|
component: RouteComponent,
|
||||||
})
|
})
|
||||||
|
|
||||||
function RouteComponent() {
|
function RouteComponent() {
|
||||||
const { planId } = useParams({ from: '/planes/$planId/_detalle/documento' })
|
|
||||||
const handleDownloadPdf = async () => {
|
|
||||||
console.log('entre aqui ')
|
|
||||||
|
|
||||||
try {
|
|
||||||
const pdfBlob = await fetchPlanPdf({
|
|
||||||
plan_estudio_id: planId,
|
|
||||||
})
|
|
||||||
|
|
||||||
const url = window.URL.createObjectURL(pdfBlob)
|
|
||||||
const link = document.createElement('a')
|
|
||||||
link.href = url
|
|
||||||
link.download = 'plan_estudios.pdf'
|
|
||||||
document.body.appendChild(link)
|
|
||||||
link.click()
|
|
||||||
|
|
||||||
link.remove()
|
|
||||||
window.URL.revokeObjectURL(url)
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error)
|
|
||||||
alert('No se pudo generar el PDF')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen flex-col gap-6 bg-slate-50/30 p-6">
|
<div className="flex flex-col gap-6 p-6 bg-slate-50/30 min-h-screen">
|
||||||
|
|
||||||
{/* HEADER DE ACCIONES */}
|
{/* HEADER DE ACCIONES */}
|
||||||
<div className="flex flex-col items-start justify-between gap-4 md:flex-row md:items-center">
|
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-xl font-bold text-slate-800">
|
<h1 className="text-xl font-bold text-slate-800">Documento del Plan</h1>
|
||||||
Documento del Plan
|
<p className="text-sm text-muted-foreground">Vista previa y descarga del documento oficial</p>
|
||||||
</h1>
|
|
||||||
<p className="text-muted-foreground text-sm">
|
|
||||||
Vista previa y descarga del documento oficial
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Button variant="outline" size="sm" className="gap-2">
|
<Button variant="outline" size="sm" className="gap-2">
|
||||||
@@ -60,18 +32,14 @@ function RouteComponent() {
|
|||||||
<Button variant="outline" size="sm" className="gap-2">
|
<Button variant="outline" size="sm" className="gap-2">
|
||||||
<Download size={16} /> Descargar Word
|
<Download size={16} /> Descargar Word
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button size="sm" className="gap-2 bg-teal-700 hover:bg-teal-800">
|
||||||
size="sm"
|
|
||||||
className="gap-2 bg-teal-700 hover:bg-teal-800"
|
|
||||||
onClick={handleDownloadPdf}
|
|
||||||
>
|
|
||||||
<Download size={16} /> Descargar PDF
|
<Download size={16} /> Descargar PDF
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* TARJETAS DE ESTADO */}
|
{/* TARJETAS DE ESTADO */}
|
||||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
<StatusCard
|
<StatusCard
|
||||||
icon={<CheckCircle2 className="text-green-500" />}
|
icon={<CheckCircle2 className="text-green-500" />}
|
||||||
label="Estado"
|
label="Estado"
|
||||||
@@ -90,69 +58,54 @@ function RouteComponent() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* CONTENEDOR DEL DOCUMENTO (Visor) */}
|
{/* CONTENEDOR DEL DOCUMENTO (Visor) */}
|
||||||
<Card className="overflow-hidden border-slate-200 shadow-sm">
|
<Card className="border-slate-200 shadow-sm overflow-hidden">
|
||||||
<div className="flex items-center justify-between border-b bg-slate-100/50 p-2 px-4">
|
<div className="bg-slate-100/50 p-2 border-b flex justify-between items-center px-4">
|
||||||
<div className="flex items-center gap-2 text-xs font-medium text-slate-500">
|
<div className="flex items-center gap-2 text-xs text-slate-500 font-medium">
|
||||||
<FileText size={14} />
|
<FileText size={14} />
|
||||||
Plan_Estudios_ISC_2024.pdf
|
Plan_Estudios_ISC_2024.pdf
|
||||||
</div>
|
</div>
|
||||||
<Button variant="ghost" size="sm" className="h-7 gap-1 text-xs">
|
<Button variant="ghost" size="sm" className="text-xs gap-1 h-7">
|
||||||
Abrir en nueva pestaña <ExternalLink size={12} />
|
Abrir en nueva pestaña <ExternalLink size={12} />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<CardContent className="flex min-h-[800px] justify-center bg-slate-200/50 p-0 py-8">
|
<CardContent className="p-0 bg-slate-200/50 flex justify-center py-8 min-h-[800px]">
|
||||||
{/* SIMULACIÓN DE HOJA DE PAPEL */}
|
{/* SIMULACIÓN DE HOJA DE PAPEL */}
|
||||||
<div className="relative min-h-[1000px] w-full max-w-[800px] border bg-white p-12 shadow-2xl md:p-16">
|
<div className="bg-white w-full max-w-[800px] shadow-2xl p-12 md:p-16 min-h-[1000px] border relative">
|
||||||
|
|
||||||
{/* Contenido del Plan */}
|
{/* Contenido del Plan */}
|
||||||
<div className="mb-12 text-center">
|
<div className="text-center mb-12">
|
||||||
<p className="mb-1 text-xs font-bold tracking-widest text-slate-400 uppercase">
|
<p className="text-xs uppercase tracking-widest text-slate-400 font-bold mb-1">Universidad Tecnológica</p>
|
||||||
Universidad Tecnológica
|
<h2 className="text-2xl font-bold text-slate-800">Plan de Estudios 2024</h2>
|
||||||
</p>
|
<h3 className="text-lg text-teal-700 font-semibold">Ingeniería en Sistemas Computacionales</h3>
|
||||||
<h2 className="text-2xl font-bold text-slate-800">
|
<p className="text-xs text-slate-500 mt-1">Facultad de Ingeniería</p>
|
||||||
Plan de Estudios 2024
|
|
||||||
</h2>
|
|
||||||
<h3 className="text-lg font-semibold text-teal-700">
|
|
||||||
Ingeniería en Sistemas Computacionales
|
|
||||||
</h3>
|
|
||||||
<p className="mt-1 text-xs text-slate-500">
|
|
||||||
Facultad de Ingeniería
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-8 text-slate-700">
|
<div className="space-y-8 text-slate-700">
|
||||||
<section>
|
<section>
|
||||||
<h4 className="mb-2 text-sm font-bold">1. Objetivo General</h4>
|
<h4 className="font-bold text-sm mb-2">1. Objetivo General</h4>
|
||||||
<p className="text-justify text-sm leading-relaxed">
|
<p className="text-sm leading-relaxed text-justify">
|
||||||
Formar profesionales altamente capacitados en el desarrollo de
|
Formar profesionales altamente capacitados en el desarrollo de soluciones tecnológicas innovadoras, con sólidos conocimientos en programación, bases de datos, redes y seguridad informática.
|
||||||
soluciones tecnológicas innovadoras, con sólidos conocimientos
|
|
||||||
en programación, bases de datos, redes y seguridad
|
|
||||||
informática.
|
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<h4 className="mb-2 text-sm font-bold">2. Perfil de Ingreso</h4>
|
<h4 className="font-bold text-sm mb-2">2. Perfil de Ingreso</h4>
|
||||||
<p className="text-justify text-sm leading-relaxed">
|
<p className="text-sm leading-relaxed text-justify">
|
||||||
Egresados de educación media superior con conocimientos
|
Egresados de educación media superior con conocimientos básicos de matemáticas, razonamiento lógico y habilidades de comunicación. Interés por la tecnología y la resolución de problemas.
|
||||||
básicos de matemáticas, razonamiento lógico y habilidades de
|
|
||||||
comunicación. Interés por la tecnología y la resolución de
|
|
||||||
problemas.
|
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<h4 className="mb-2 text-sm font-bold">3. Perfil de Egreso</h4>
|
<h4 className="font-bold text-sm mb-2">3. Perfil de Egreso</h4>
|
||||||
<p className="text-justify text-sm leading-relaxed">
|
<p className="text-sm leading-relaxed text-justify">
|
||||||
Profesional capaz de diseñar, desarrollar e implementar
|
Profesional capaz de diseñar, desarrollar e implementar sistemas de software de calidad, administrar infraestructuras de red y liderar proyectos tecnológicos multidisciplinarios.
|
||||||
sistemas de software de calidad, administrar infraestructuras
|
|
||||||
de red y liderar proyectos tecnológicos multidisciplinarios.
|
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Marca de agua o decoración lateral (opcional) */}
|
{/* Marca de agua o decoración lateral (opcional) */}
|
||||||
<div className="absolute top-0 left-0 h-full w-1 bg-slate-100" />
|
<div className="absolute top-0 left-0 w-1 h-full bg-slate-100" />
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -161,23 +114,15 @@ function RouteComponent() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Componente pequeño para las tarjetas de estado superior
|
// Componente pequeño para las tarjetas de estado superior
|
||||||
function StatusCard({
|
function StatusCard({ icon, label, value }: { icon: React.ReactNode, label: string, value: string }) {
|
||||||
icon,
|
|
||||||
label,
|
|
||||||
value,
|
|
||||||
}: {
|
|
||||||
icon: React.ReactNode
|
|
||||||
label: string
|
|
||||||
value: string
|
|
||||||
}) {
|
|
||||||
return (
|
return (
|
||||||
<Card className="border-slate-200 bg-white">
|
<Card className="bg-white border-slate-200">
|
||||||
<CardContent className="flex items-center gap-4 p-4">
|
<CardContent className="p-4 flex items-center gap-4">
|
||||||
<div className="rounded-full border bg-slate-50 p-2">{icon}</div>
|
<div className="p-2 rounded-full bg-slate-50 border">
|
||||||
|
{icon}
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-[10px] font-bold tracking-tight text-slate-400 uppercase">
|
<p className="text-[10px] uppercase font-bold text-slate-400 tracking-tight">{label}</p>
|
||||||
{label}
|
|
||||||
</p>
|
|
||||||
<p className="text-sm font-semibold text-slate-700">{value}</p>
|
<p className="text-sm font-semibold text-slate-700">{value}</p>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@@ -177,29 +177,7 @@ function MapaCurricularPage() {
|
|||||||
const manejarAgregarLinea = (nombre: string) => {
|
const manejarAgregarLinea = (nombre: string) => {
|
||||||
const nombreNormalizado = nombre.trim()
|
const nombreNormalizado = nombre.trim()
|
||||||
|
|
||||||
// 1. Validar que no esté vacío
|
// Validar si es Área Común (insensible a mayúsculas/minúsculas)
|
||||||
if (!nombreNormalizado) return
|
|
||||||
|
|
||||||
// 2. Validar duplicados (Insensible a mayúsculas/minúsculas y acentos)
|
|
||||||
const nombreParaComparar = nombreNormalizado
|
|
||||||
.toLowerCase()
|
|
||||||
.normalize('NFD')
|
|
||||||
.replace(/[\u0300-\u036f]/g, '')
|
|
||||||
|
|
||||||
const yaExiste = lineas.some((l) => {
|
|
||||||
const lineaNombreBase = l.nombre
|
|
||||||
.toLowerCase()
|
|
||||||
.normalize('NFD')
|
|
||||||
.replace(/[\u0300-\u036f]/g, '')
|
|
||||||
return lineaNombreBase === nombreParaComparar
|
|
||||||
})
|
|
||||||
|
|
||||||
if (yaExiste) {
|
|
||||||
alert(`La línea "${nombreNormalizado}" ya existe.`)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Validar Área Común (usando tu lógica previa)
|
|
||||||
const esAreaComun =
|
const esAreaComun =
|
||||||
nombreNormalizado.toLowerCase() === 'área común' ||
|
nombreNormalizado.toLowerCase() === 'área común' ||
|
||||||
nombreNormalizado.toLowerCase() === 'area comun'
|
nombreNormalizado.toLowerCase() === 'area comun'
|
||||||
@@ -209,12 +187,10 @@ function MapaCurricularPage() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Agregar la línea si todo está bien
|
|
||||||
const nueva = {
|
const nueva = {
|
||||||
id: crypto.randomUUID(),
|
id: crypto.randomUUID(),
|
||||||
nombre: nombreNormalizado,
|
nombre: nombreNormalizado,
|
||||||
orden: lineas.length + 1,
|
orden: lineas.length + 1,
|
||||||
color: '#1976d2',
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setLineas([...lineas, nueva])
|
setLineas([...lineas, nueva])
|
||||||
@@ -222,7 +198,6 @@ function MapaCurricularPage() {
|
|||||||
if (esAreaComun) {
|
if (esAreaComun) {
|
||||||
setHasAreaComun(true)
|
setHasAreaComun(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
setNombreNuevaLinea('') // Limpiar input
|
setNombreNuevaLinea('') // Limpiar input
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -624,46 +599,19 @@ function MapaCurricularPage() {
|
|||||||
<label className="text-xs font-bold text-slate-500 uppercase">
|
<label className="text-xs font-bold text-slate-500 uppercase">
|
||||||
Créditos
|
Créditos
|
||||||
</label>
|
</label>
|
||||||
<Input
|
<Input type="number" value={editingData.creditos} />
|
||||||
type="number"
|
|
||||||
value={editingData.creditos}
|
|
||||||
onChange={(e) =>
|
|
||||||
setEditingData({
|
|
||||||
...editingData,
|
|
||||||
creditos: Number(e.target.value),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-xs font-bold text-slate-500 uppercase">
|
<label className="text-xs font-bold text-slate-500 uppercase">
|
||||||
HD (Horas Docente)
|
HD (Horas Docente)
|
||||||
</label>
|
</label>
|
||||||
<Input
|
<Input type="number" value={editingData.hd} />
|
||||||
type="number"
|
|
||||||
value={editingData.hd}
|
|
||||||
onChange={(e) =>
|
|
||||||
setEditingData({
|
|
||||||
...editingData,
|
|
||||||
hd: Number(e.target.value),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-xs font-bold text-slate-500 uppercase">
|
<label className="text-xs font-bold text-slate-500 uppercase">
|
||||||
HI (Horas Indep.)
|
HI (Horas Indep.)
|
||||||
</label>
|
</label>
|
||||||
<Input
|
<Input type="number" value={editingData.hi} />
|
||||||
type="number"
|
|
||||||
value={editingData.hi}
|
|
||||||
onChange={(e) =>
|
|
||||||
setEditingData({
|
|
||||||
...editingData,
|
|
||||||
hi: Number(e.target.value),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -673,22 +621,11 @@ function MapaCurricularPage() {
|
|||||||
<label className="text-xs font-bold text-slate-500 uppercase">
|
<label className="text-xs font-bold text-slate-500 uppercase">
|
||||||
Ciclo
|
Ciclo
|
||||||
</label>
|
</label>
|
||||||
<Select
|
<Select value={editingData.ciclo?.toString() || 'null'}>
|
||||||
value={editingData.ciclo?.toString() || 'unassigned'}
|
|
||||||
onValueChange={(val) =>
|
|
||||||
setEditingData({
|
|
||||||
...editingData,
|
|
||||||
ciclo: val === 'unassigned' ? null : Number(val),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="unassigned">
|
|
||||||
-- Sin Asignar --
|
|
||||||
</SelectItem>
|
|
||||||
{ciclosArray.map((n) => (
|
{ciclosArray.map((n) => (
|
||||||
<SelectItem key={n} value={n.toString()}>
|
<SelectItem key={n} value={n.toString()}>
|
||||||
Ciclo {n}
|
Ciclo {n}
|
||||||
@@ -697,27 +634,15 @@ function MapaCurricularPage() {
|
|||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-xs font-bold text-slate-500 uppercase">
|
<label className="text-xs font-bold text-slate-500 uppercase">
|
||||||
Línea Curricular
|
Línea Curricular
|
||||||
</label>
|
</label>
|
||||||
<Select
|
<Select value={editingData.lineaCurricularId || 'null'}>
|
||||||
value={editingData.lineaCurricularId || 'unassigned'}
|
|
||||||
onValueChange={(val) =>
|
|
||||||
setEditingData({
|
|
||||||
...editingData,
|
|
||||||
lineaCurricularId: val === 'unassigned' ? null : val,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="unassigned">
|
|
||||||
-- Sin Asignar --
|
|
||||||
</SelectItem>
|
|
||||||
{lineas.map((l) => (
|
{lineas.map((l) => (
|
||||||
<SelectItem key={l.id} value={l.id}>
|
<SelectItem key={l.id} value={l.id}>
|
||||||
{l.nombre}
|
{l.nombre}
|
||||||
@@ -764,12 +689,7 @@ function MapaCurricularPage() {
|
|||||||
<label className="text-xs font-bold text-slate-500 uppercase">
|
<label className="text-xs font-bold text-slate-500 uppercase">
|
||||||
Tipo
|
Tipo
|
||||||
</label>
|
</label>
|
||||||
<Select
|
<Select value={editingData.tipo}>
|
||||||
value={editingData.tipo}
|
|
||||||
onValueChange={(val: 'obligatoria' | 'optativa') =>
|
|
||||||
setEditingData({ ...editingData, tipo: val })
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
|
|||||||
@@ -233,7 +233,6 @@ function MateriasPage() {
|
|||||||
},
|
},
|
||||||
state: {
|
state: {
|
||||||
realId: materia.id, // 👈 ID largo oculto
|
realId: materia.id, // 👈 ID largo oculto
|
||||||
asignaturaId: materia.id,
|
|
||||||
} as any,
|
} as any,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,20 +5,12 @@ import {
|
|||||||
Clock,
|
Clock,
|
||||||
Hash,
|
Hash,
|
||||||
CalendarDays,
|
CalendarDays,
|
||||||
Save,
|
Rocket,
|
||||||
|
BookOpen,
|
||||||
|
CheckCircle2,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { useState, useEffect } from 'react'
|
|
||||||
|
|
||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
import { Button } from '@/components/ui/button'
|
|
||||||
import {
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
} from '@/components/ui/dropdown-menu'
|
|
||||||
import { Skeleton } from '@/components/ui/skeleton'
|
|
||||||
import { usePlan } from '@/data/hooks/usePlans'
|
|
||||||
|
|
||||||
export const Route = createFileRoute('/planes/$planId/_detalle')({
|
export const Route = createFileRoute('/planes/$planId/_detalle')({
|
||||||
component: RouteComponent,
|
component: RouteComponent,
|
||||||
@@ -26,55 +18,10 @@ export const Route = createFileRoute('/planes/$planId/_detalle')({
|
|||||||
|
|
||||||
function RouteComponent() {
|
function RouteComponent() {
|
||||||
const { planId } = Route.useParams()
|
const { planId } = Route.useParams()
|
||||||
const { data, isLoading } = usePlan(planId)
|
|
||||||
|
|
||||||
// Estados locales para manejar la edición "en vivo" antes de persistir
|
|
||||||
const [nombrePlan, setNombrePlan] = useState('')
|
|
||||||
const [nivelPlan, setNivelPlan] = useState('')
|
|
||||||
const [isDirty, setIsDirty] = useState(false)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (data) {
|
|
||||||
setNombrePlan(data.nombre || '')
|
|
||||||
setNivelPlan(data.nivel || '')
|
|
||||||
}
|
|
||||||
}, [data])
|
|
||||||
|
|
||||||
const niveles = [
|
|
||||||
'Licenciatura',
|
|
||||||
'Maestría',
|
|
||||||
'Doctorado',
|
|
||||||
'Diplomado',
|
|
||||||
'Especialidad',
|
|
||||||
]
|
|
||||||
|
|
||||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
||||||
if (e.key === 'Enter') {
|
|
||||||
e.preventDefault() // Evita el salto de línea
|
|
||||||
e.currentTarget.blur() // Quita el foco, lo que dispara el onBlur y "guarda" en el estado
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSave = () => {
|
|
||||||
console.log('Guardando en DB...', { nombrePlan, nivelPlan })
|
|
||||||
// Aquí iría tu mutation
|
|
||||||
setIsDirty(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-white">
|
<div className="min-h-screen bg-white">
|
||||||
{/* Botón Flotante de Guardar */}
|
{/* 1. Header Superior con Sombra (Volver a planes) */}
|
||||||
{isDirty && (
|
|
||||||
<div className="animate-in fade-in slide-in-from-bottom-4 fixed right-8 bottom-8 z-50 duration-300">
|
|
||||||
<Button
|
|
||||||
onClick={handleSave}
|
|
||||||
className="gap-2 rounded-full bg-teal-600 px-6 shadow-xl hover:bg-teal-700"
|
|
||||||
>
|
|
||||||
<Save size={16} /> Guardar cambios del Plan
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{/* 1. Header Superior */}
|
|
||||||
<div className="sticky top-0 z-20 border-b bg-white/50 shadow-sm backdrop-blur-sm">
|
<div className="sticky top-0 z-20 border-b bg-white/50 shadow-sm backdrop-blur-sm">
|
||||||
<div className="px-6 py-2">
|
<div className="px-6 py-2">
|
||||||
<Link
|
<Link
|
||||||
@@ -86,93 +33,50 @@ function RouteComponent() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 2. Contenido Principal con Padding */}
|
||||||
<div className="mx-auto max-w-[1600px] space-y-8 p-8">
|
<div className="mx-auto max-w-[1600px] space-y-8 p-8">
|
||||||
{/* Header del Plan */}
|
{/* Header del Plan y Badges */}
|
||||||
{isLoading ? (
|
|
||||||
/* ===== SKELETON ===== */
|
|
||||||
<div className="mx-auto max-w-[1600px] p-8">
|
|
||||||
<div className="grid grid-cols-1 gap-6 lg:grid-cols-2">
|
|
||||||
{Array.from({ length: 6 }).map((_, i) => (
|
|
||||||
<DatosGeneralesSkeleton key={i} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<div className="flex flex-col items-start justify-between gap-4 md:flex-row">
|
<div className="flex flex-col items-start justify-between gap-4 md:flex-row">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="flex items-baseline gap-2 text-3xl font-bold tracking-tight text-slate-900">
|
<h1 className="text-3xl font-bold tracking-tight text-slate-900">
|
||||||
<span>{nivelPlan} en</span>
|
Plan de Estudios 2024
|
||||||
<span
|
|
||||||
role="textbox"
|
|
||||||
tabIndex={0}
|
|
||||||
contentEditable
|
|
||||||
suppressContentEditableWarning
|
|
||||||
spellCheck={false} // Quita el subrayado rojo de error ortográfico
|
|
||||||
onKeyDown={handleKeyDown}
|
|
||||||
onBlur={(e) =>
|
|
||||||
setNombrePlan(e.currentTarget.textContent || '')
|
|
||||||
}
|
|
||||||
className="cursor-text border-b border-transparent decoration-transparent transition-colors outline-none select-text hover:border-slate-300 focus:border-teal-500"
|
|
||||||
style={{
|
|
||||||
WebkitTextDecoration: 'none',
|
|
||||||
textDecoration: 'none',
|
|
||||||
}} // Doble seguridad contra subrayados
|
|
||||||
>
|
|
||||||
{nombrePlan}
|
|
||||||
</span>
|
|
||||||
</h1>
|
</h1>
|
||||||
<p className="mt-1 text-lg font-medium text-slate-500">
|
<p className="mt-1 text-lg font-medium text-slate-500">
|
||||||
{data?.carreras?.facultades?.nombre}{' '}
|
Ingeniería en Sistemas Computacionales
|
||||||
{data?.carreras?.nombre_corto}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Badges de la derecha */}
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
{/* <Badge className="gap-1 border-teal-200 bg-teal-50 px-3 text-teal-700 hover:bg-teal-100">
|
|
||||||
<CheckCircle2 size={12} /> {data?.estados_plan?.etiqueta}
|
|
||||||
</Badge> */}
|
|
||||||
<Badge
|
<Badge
|
||||||
className={`gap-1 border-teal-200 bg-teal-50 px-3 text-teal-700 hover:bg-teal-100`}
|
variant="secondary"
|
||||||
|
className="gap-1 border-blue-100 bg-blue-50 px-3 text-blue-700"
|
||||||
>
|
>
|
||||||
{data?.estados_plan?.etiqueta}
|
<Rocket size={12} /> Ingeniería
|
||||||
|
</Badge>
|
||||||
|
<Badge
|
||||||
|
variant="secondary"
|
||||||
|
className="gap-1 border-orange-100 bg-orange-50 px-3 text-orange-700"
|
||||||
|
>
|
||||||
|
<BookOpen size={12} /> Licenciatura
|
||||||
|
</Badge>
|
||||||
|
<Badge className="gap-1 border-teal-200 bg-teal-50 px-3 text-teal-700 hover:bg-teal-100">
|
||||||
|
<CheckCircle2 size={12} /> En Revisión
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 3. Cards de Información con Context Menu */}
|
{/* 3. Cards de Información (Nivel, Duración, etc.) */}
|
||||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-4">
|
<div className="grid grid-cols-2 gap-4 md:grid-cols-4">
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger>
|
|
||||||
<InfoCard
|
<InfoCard
|
||||||
icon={<GraduationCap className="text-slate-400" />}
|
icon={<GraduationCap className="text-slate-400" />}
|
||||||
label="Nivel"
|
label="Nivel"
|
||||||
value={nivelPlan}
|
value="Superior"
|
||||||
isEditable
|
|
||||||
/>
|
/>
|
||||||
</DropdownMenuTrigger>
|
|
||||||
|
|
||||||
<DropdownMenuContent className="w-48">
|
|
||||||
{niveles.map((n) => (
|
|
||||||
<DropdownMenuItem
|
|
||||||
key={n}
|
|
||||||
onClick={() => {
|
|
||||||
setNivelPlan(n)
|
|
||||||
setIsDirty(true)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{n}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
))}
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
|
|
||||||
<InfoCard
|
<InfoCard
|
||||||
icon={<Clock className="text-slate-400" />}
|
icon={<Clock className="text-slate-400" />}
|
||||||
label="Duración"
|
label="Duración"
|
||||||
value={`${data?.numero_ciclos || 0} Ciclos`}
|
value="9 Semestres"
|
||||||
/>
|
/>
|
||||||
<InfoCard
|
<InfoCard
|
||||||
icon={<Hash className="text-slate-400" />}
|
icon={<Hash className="text-slate-400" />}
|
||||||
@@ -182,7 +86,7 @@ function RouteComponent() {
|
|||||||
<InfoCard
|
<InfoCard
|
||||||
icon={<CalendarDays className="text-slate-400" />}
|
icon={<CalendarDays className="text-slate-400" />}
|
||||||
label="Creación"
|
label="Creación"
|
||||||
value={data?.creado_en?.split('T')[0]} // Cortamos la fecha para que no sea tan larga
|
value="14 ene 2024"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -213,6 +117,7 @@ function RouteComponent() {
|
|||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 5. Contenido del Tab */}
|
||||||
<main className="animate-in fade-in pt-2 duration-500">
|
<main className="animate-in fade-in pt-2 duration-500">
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</main>
|
</main>
|
||||||
@@ -221,37 +126,24 @@ function RouteComponent() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sub-componente para las tarjetas de información
|
||||||
function InfoCard({
|
function InfoCard({
|
||||||
icon,
|
icon,
|
||||||
label,
|
label,
|
||||||
value,
|
value,
|
||||||
isEditable,
|
|
||||||
}: {
|
}: {
|
||||||
icon: React.ReactNode
|
icon: React.ReactNode
|
||||||
label: string
|
label: string
|
||||||
value: string | number | undefined
|
value: string
|
||||||
isEditable?: boolean
|
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="flex items-center gap-4 rounded-xl border border-slate-200/60 bg-slate-50/50 p-4 shadow-sm">
|
||||||
className={`flex h-[72px] w-full items-center gap-4 rounded-xl border border-slate-200/60 bg-slate-50/50 p-4 shadow-sm transition-all ${
|
<div className="rounded-lg border bg-white p-2 shadow-sm">{icon}</div>
|
||||||
isEditable
|
<div>
|
||||||
? 'cursor-pointer hover:border-teal-200 hover:bg-white focus:outline-none focus-visible:ring-2 focus-visible:ring-teal-500/40'
|
<p className="mb-1 text-[10px] leading-none font-bold tracking-wider text-slate-400 uppercase">
|
||||||
: ''
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg border bg-white shadow-sm">
|
|
||||||
{icon}
|
|
||||||
</div>
|
|
||||||
<div className="min-w-0 flex-1">
|
|
||||||
{' '}
|
|
||||||
{/* min-w-0 es vital para que el truncate funcione en flex */}
|
|
||||||
<p className="mb-0.5 truncate text-[10px] font-bold tracking-wider text-slate-400 uppercase">
|
|
||||||
{label}
|
{label}
|
||||||
</p>
|
</p>
|
||||||
<p className="truncate text-sm font-semibold text-slate-700">
|
<p className="text-sm font-semibold text-slate-700">{value}</p>
|
||||||
{value || '---'}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -271,29 +163,11 @@ function Tab({
|
|||||||
to={to}
|
to={to}
|
||||||
params={params}
|
params={params}
|
||||||
className="border-b-2 border-transparent pb-3 text-sm font-medium text-slate-500 transition-all hover:text-slate-800"
|
className="border-b-2 border-transparent pb-3 text-sm font-medium text-slate-500 transition-all hover:text-slate-800"
|
||||||
activeProps={{ className: 'border-teal-600 text-teal-700 font-bold' }}
|
activeProps={{
|
||||||
|
className: 'border-teal-600 text-teal-700 font-bold',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</Link>
|
</Link>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function DatosGeneralesSkeleton() {
|
|
||||||
return (
|
|
||||||
<div className="rounded-xl border bg-white">
|
|
||||||
{/* Header */}
|
|
||||||
<div className="flex items-center justify-between border-b px-5 py-3">
|
|
||||||
<Skeleton className="h-4 w-40" />
|
|
||||||
<Skeleton className="h-8 w-16" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Content */}
|
|
||||||
<div className="space-y-3 p-5">
|
|
||||||
<Skeleton className="h-4 w-full" />
|
|
||||||
<Skeleton className="h-4 w-11/12" />
|
|
||||||
<Skeleton className="h-4 w-10/12" />
|
|
||||||
<Skeleton className="h-4 w-9/12" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { createFileRoute, redirect } from '@tanstack/react-router'
|
|||||||
export const Route = createFileRoute('/planes/$planId/')({
|
export const Route = createFileRoute('/planes/$planId/')({
|
||||||
beforeLoad: ({ params }) => {
|
beforeLoad: ({ params }) => {
|
||||||
throw redirect({
|
throw redirect({
|
||||||
to: '/planes/$planId/materias',
|
to: '/planes/$planId/datos',
|
||||||
params,
|
params,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user