diff --git a/src/components/asignaturas/detalle/BibliographyItem.tsx b/src/components/asignaturas/detalle/BibliographyItem.tsx index 928b662..1d08370 100644 --- a/src/components/asignaturas/detalle/BibliographyItem.tsx +++ b/src/components/asignaturas/detalle/BibliographyItem.tsx @@ -85,6 +85,7 @@ export interface BibliografiaEntry { } interface BibliografiaTabProps { + id: string bibliografia: Array onSave: (bibliografia: Array) => void isSaving: boolean @@ -92,12 +93,14 @@ interface BibliografiaTabProps { export function BibliographyItem({ bibliografia, - asignaturaId, + id, onSave, isSaving, }: BibliografiaTabProps) { + console.log(id) + const { data: bibliografia2, isLoading: loadinmateria } = - useSubjectBibliografia(asignaturaId) + useSubjectBibliografia(id) const [entries, setEntries] = useState>(bibliografia) const [isAddDialogOpen, setIsAddDialogOpen] = useState(false) const [isLibraryDialogOpen, setIsLibraryDialogOpen] = useState(false) diff --git a/src/components/asignaturas/detalle/MateriaDetailPage.tsx b/src/components/asignaturas/detalle/MateriaDetailPage.tsx index 1922bb5..e0594dd 100644 --- a/src/components/asignaturas/detalle/MateriaDetailPage.tsx +++ b/src/components/asignaturas/detalle/MateriaDetailPage.tsx @@ -36,6 +36,7 @@ export interface BibliografiaEntry { fuenteBiblioteca?: any } export interface BibliografiaTabProps { + id: string bibliografia: Array onSave: (bibliografia: Array) => void isSaving: boolean @@ -58,27 +59,13 @@ function EditableHeaderField({ onSave: (val: string) => void 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) => { - const newValue = e.currentTarget.textContent || '' - if (newValue !== value.toString()) { - onSave(newValue) - } - } - return ( onSave(e.target.value)} onBlur={(e) => onSave(e.target.value)} - className={`border-none bg-transparent outline-none focus:ring-2 focus:ring-blue-400 ${className}`} + 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 ?? ''} `} /> ) } @@ -240,12 +227,14 @@ export default function MateriaDetailPage() {
{/* CRÉDITOS EDITABLES */} - - handleUpdateHeader('creditos', parseInt(val) || 0) - } - /> + + + handleUpdateHeader('creditos', parseInt(val) || 0) + } + /> + créditos diff --git a/src/components/ui/skeleton.tsx b/src/components/ui/skeleton.tsx new file mode 100644 index 0000000..32ea0ef --- /dev/null +++ b/src/components/ui/skeleton.tsx @@ -0,0 +1,13 @@ +import { cn } from "@/lib/utils" + +function Skeleton({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +export { Skeleton } diff --git a/src/data/api/document.api.ts b/src/data/api/document.api.ts new file mode 100644 index 0000000..fb59770 --- /dev/null +++ b/src/data/api/document.api.ts @@ -0,0 +1,27 @@ +// 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 { + 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() +} diff --git a/src/routes/planes/$planId/_detalle/datos.tsx b/src/routes/planes/$planId/_detalle/datos.tsx index e071db1..200553a 100644 --- a/src/routes/planes/$planId/_detalle/datos.tsx +++ b/src/routes/planes/$planId/_detalle/datos.tsx @@ -19,7 +19,7 @@ const formatLabel = (key: string) => { function DatosGeneralesPage() { const { planId } = Route.useParams() - const { data } = usePlan(planId) + const { data, isLoading } = usePlan(planId) const navigate = useNavigate() // Inicializamos campos como un arreglo vacío const [campos, setCampos] = useState>([]) diff --git a/src/routes/planes/$planId/_detalle/documento.tsx b/src/routes/planes/$planId/_detalle/documento.tsx index 0082d77..dba0d3d 100644 --- a/src/routes/planes/$planId/_detalle/documento.tsx +++ b/src/routes/planes/$planId/_detalle/documento.tsx @@ -1,29 +1,57 @@ -import { createFileRoute } from '@tanstack/react-router' -import { - FileText, - Download, - RefreshCcw, - ExternalLink, - CheckCircle2, - Clock, - FileJson -} from "lucide-react" -import { Button } from "@/components/ui/button" -import { Card, CardContent } from "@/components/ui/card" +import { createFileRoute, useParams } from '@tanstack/react-router' +import { + FileText, + Download, + RefreshCcw, + ExternalLink, + CheckCircle2, + Clock, + FileJson, +} from 'lucide-react' + +import { Button } from '@/components/ui/button' +import { Card, CardContent } from '@/components/ui/card' +import { fetchPlanPdf } from '@/data/api/document.api' export const Route = createFileRoute('/planes/$planId/_detalle/documento')({ component: 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 ( -
- +
{/* HEADER DE ACCIONES */} -
+
-

Documento del Plan

-

Vista previa y descarga del documento oficial

+

+ Documento del Plan +

+

+ Vista previa y descarga del documento oficial +

-
{/* TARJETAS DE ESTADO */} -
- } - label="Estado" - value="Generado" +
+ } + label="Estado" + value="Generado" /> - } - label="Última generación" - value="28 Ene 2024, 11:30" + } + label="Última generación" + value="28 Ene 2024, 11:30" /> - } - label="Versión" - value="v1.2" + } + label="Versión" + value="v1.2" />
{/* CONTENEDOR DEL DOCUMENTO (Visor) */} - -
-
+ +
+
Plan_Estudios_ISC_2024.pdf
-
- - + + {/* SIMULACIÓN DE HOJA DE PAPEL */} -
- +
{/* Contenido del Plan */} -
-

Universidad Tecnológica

-

Plan de Estudios 2024

-

Ingeniería en Sistemas Computacionales

-

Facultad de Ingeniería

+
+

+ Universidad Tecnológica +

+

+ Plan de Estudios 2024 +

+

+ Ingeniería en Sistemas Computacionales +

+

+ Facultad de Ingeniería +

-

1. Objetivo General

-

- 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. +

1. Objetivo General

+

+ 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.

-

2. Perfil de Ingreso

-

- 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. +

2. Perfil de Ingreso

+

+ 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.

-

3. Perfil de Egreso

-

- Profesional capaz de diseñar, desarrollar e implementar sistemas de software de calidad, administrar infraestructuras de red y liderar proyectos tecnológicos multidisciplinarios. +

3. Perfil de Egreso

+

+ Profesional capaz de diseñar, desarrollar e implementar + sistemas de software de calidad, administrar infraestructuras + de red y liderar proyectos tecnológicos multidisciplinarios.

{/* Marca de agua o decoración lateral (opcional) */} -
+
@@ -114,18 +161,26 @@ function RouteComponent() { } // Componente pequeño para las tarjetas de estado superior -function StatusCard({ icon, label, value }: { icon: React.ReactNode, label: string, value: string }) { +function StatusCard({ + icon, + label, + value, +}: { + icon: React.ReactNode + label: string + value: string +}) { return ( - - -
- {icon} -
+ + +
{icon}
-

{label}

+

+ {label} +

{value}

) -} \ No newline at end of file +} diff --git a/src/routes/planes/$planId/_detalle/route.tsx b/src/routes/planes/$planId/_detalle/route.tsx index 25c3aeb..90eb180 100644 --- a/src/routes/planes/$planId/_detalle/route.tsx +++ b/src/routes/planes/$planId/_detalle/route.tsx @@ -12,11 +12,12 @@ import { useState, useEffect } from 'react' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { - ContextMenu, - ContextMenuContent, - ContextMenuItem, - ContextMenuTrigger, -} from '@/components/ui/context-menu' + 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')({ @@ -25,7 +26,7 @@ export const Route = createFileRoute('/planes/$planId/_detalle')({ function RouteComponent() { const { planId } = Route.useParams() - const { data } = usePlan(planId) + const { data, isLoading } = usePlan(planId) // Estados locales para manejar la edición "en vivo" antes de persistir const [nombrePlan, setNombrePlan] = useState('') @@ -87,63 +88,86 @@ function RouteComponent() {
{/* Header del Plan */} -
-
-

- {nivelPlan} en - 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} - -

-

- {data?.carreras?.facultades?.nombre}{' '} - {data?.carreras?.nombre_corto} -

+ {isLoading ? ( + /* ===== SKELETON ===== */ +
+
+ {Array.from({ length: 6 }).map((_, i) => ( + + ))} +
+ ) : ( + <> +
+
+

+ {nivelPlan} en + + 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} + +

+

+ {data?.carreras?.facultades?.nombre}{' '} + {data?.carreras?.nombre_corto} +

+
-
- {/* +
+ {/* {data?.estados_plan?.etiqueta} */} - - {data?.estados_plan?.etiqueta} - -
-
+ + {data?.estados_plan?.etiqueta} + +
+
+ + )} {/* 3. Cards de Información con Context Menu */}
- - - {/* Eliminamos el div extra y aplicamos el estilo directamente al trigger si es necesario, - pero con asChild, la InfoCard será el trigger real */} + + } label="Nivel" value={nivelPlan} isEditable /> - - + + + {niveles.map((n) => ( - setNivelPlan(n)}> + { + setNivelPlan(n) + setIsDirty(true) + }} + > {n} - + ))} - - + + } @@ -212,7 +236,7 @@ function InfoCard({
@@ -253,3 +277,23 @@ function Tab({ ) } + +function DatosGeneralesSkeleton() { + return ( +
+ {/* Header */} +
+ + +
+ + {/* Content */} +
+ + + + +
+
+ ) +} diff --git a/src/routes/planes/$planId/index.tsx b/src/routes/planes/$planId/index.tsx index 63755ad..15ac830 100644 --- a/src/routes/planes/$planId/index.tsx +++ b/src/routes/planes/$planId/index.tsx @@ -3,7 +3,7 @@ import { createFileRoute, redirect } from '@tanstack/react-router' export const Route = createFileRoute('/planes/$planId/')({ beforeLoad: ({ params }) => { throw redirect({ - to: '/planes/$planId/datos', + to: '/planes/$planId/materias', params, }) },