Se corrigen incidencias
This commit is contained in:
@@ -85,6 +85,7 @@ export interface BibliografiaEntry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface BibliografiaTabProps {
|
interface BibliografiaTabProps {
|
||||||
|
id: string
|
||||||
bibliografia: Array<BibliografiaEntry>
|
bibliografia: Array<BibliografiaEntry>
|
||||||
onSave: (bibliografia: Array<BibliografiaEntry>) => void
|
onSave: (bibliografia: Array<BibliografiaEntry>) => void
|
||||||
isSaving: boolean
|
isSaving: boolean
|
||||||
@@ -92,12 +93,14 @@ interface BibliografiaTabProps {
|
|||||||
|
|
||||||
export function BibliographyItem({
|
export function BibliographyItem({
|
||||||
bibliografia,
|
bibliografia,
|
||||||
asignaturaId,
|
id,
|
||||||
onSave,
|
onSave,
|
||||||
isSaving,
|
isSaving,
|
||||||
}: BibliografiaTabProps) {
|
}: BibliografiaTabProps) {
|
||||||
|
console.log(id)
|
||||||
|
|
||||||
const { data: bibliografia2, isLoading: loadinmateria } =
|
const { data: bibliografia2, isLoading: loadinmateria } =
|
||||||
useSubjectBibliografia(asignaturaId)
|
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)
|
||||||
const [isLibraryDialogOpen, setIsLibraryDialogOpen] = useState(false)
|
const [isLibraryDialogOpen, setIsLibraryDialogOpen] = useState(false)
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ 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
|
||||||
@@ -58,27 +59,13 @@ 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
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={String(value)}
|
value={String(value)}
|
||||||
onChange={(e) => onSave(e.target.value)}
|
onChange={(e) => onSave(e.target.value)}
|
||||||
onBlur={(e) => 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() {
|
|||||||
<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">
|
||||||
<EditableHeaderField
|
<span className="inline-flex max-w-fit">
|
||||||
value={headerData.creditos}
|
<EditableHeaderField
|
||||||
onSave={(val) =>
|
value={headerData.creditos}
|
||||||
handleUpdateHeader('creditos', parseInt(val) || 0)
|
onSave={(val) =>
|
||||||
}
|
handleUpdateHeader('creditos', parseInt(val) || 0)
|
||||||
/>
|
}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
<span>créditos</span>
|
<span>créditos</span>
|
||||||
</Badge>
|
</Badge>
|
||||||
|
|
||||||
|
|||||||
13
src/components/ui/skeleton.tsx
Normal file
13
src/components/ui/skeleton.tsx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
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 }
|
||||||
27
src/data/api/document.api.ts
Normal file
27
src/data/api/document.api.ts
Normal file
@@ -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<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()
|
||||||
|
}
|
||||||
@@ -19,7 +19,7 @@ const formatLabel = (key: string) => {
|
|||||||
|
|
||||||
function DatosGeneralesPage() {
|
function DatosGeneralesPage() {
|
||||||
const { planId } = Route.useParams()
|
const { planId } = Route.useParams()
|
||||||
const { data } = usePlan(planId)
|
const { data, isLoading } = 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>>([])
|
||||||
|
|||||||
@@ -1,29 +1,57 @@
|
|||||||
import { createFileRoute } from '@tanstack/react-router'
|
import { createFileRoute, useParams } from '@tanstack/react-router'
|
||||||
import {
|
import {
|
||||||
FileText,
|
FileText,
|
||||||
Download,
|
Download,
|
||||||
RefreshCcw,
|
RefreshCcw,
|
||||||
ExternalLink,
|
ExternalLink,
|
||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
Clock,
|
Clock,
|
||||||
FileJson
|
FileJson,
|
||||||
} from "lucide-react"
|
} from 'lucide-react'
|
||||||
import { Button } from "@/components/ui/button"
|
|
||||||
import { Card, CardContent } from "@/components/ui/card"
|
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')({
|
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 flex-col gap-6 p-6 bg-slate-50/30 min-h-screen">
|
<div className="flex min-h-screen flex-col gap-6 bg-slate-50/30 p-6">
|
||||||
|
|
||||||
{/* HEADER DE ACCIONES */}
|
{/* HEADER DE ACCIONES */}
|
||||||
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-4">
|
<div className="flex flex-col items-start justify-between gap-4 md:flex-row md:items-center">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-xl font-bold text-slate-800">Documento del Plan</h1>
|
<h1 className="text-xl font-bold text-slate-800">
|
||||||
<p className="text-sm text-muted-foreground">Vista previa y descarga del documento oficial</p>
|
Documento del Plan
|
||||||
|
</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">
|
||||||
@@ -32,80 +60,99 @@ 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 size="sm" className="gap-2 bg-teal-700 hover:bg-teal-800">
|
<Button
|
||||||
|
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 md:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
|
||||||
<StatusCard
|
<StatusCard
|
||||||
icon={<CheckCircle2 className="text-green-500" />}
|
icon={<CheckCircle2 className="text-green-500" />}
|
||||||
label="Estado"
|
label="Estado"
|
||||||
value="Generado"
|
value="Generado"
|
||||||
/>
|
/>
|
||||||
<StatusCard
|
<StatusCard
|
||||||
icon={<Clock className="text-blue-500" />}
|
icon={<Clock className="text-blue-500" />}
|
||||||
label="Última generación"
|
label="Última generación"
|
||||||
value="28 Ene 2024, 11:30"
|
value="28 Ene 2024, 11:30"
|
||||||
/>
|
/>
|
||||||
<StatusCard
|
<StatusCard
|
||||||
icon={<FileJson className="text-orange-500" />}
|
icon={<FileJson className="text-orange-500" />}
|
||||||
label="Versión"
|
label="Versión"
|
||||||
value="v1.2"
|
value="v1.2"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* CONTENEDOR DEL DOCUMENTO (Visor) */}
|
{/* CONTENEDOR DEL DOCUMENTO (Visor) */}
|
||||||
<Card className="border-slate-200 shadow-sm overflow-hidden">
|
<Card className="overflow-hidden border-slate-200 shadow-sm">
|
||||||
<div className="bg-slate-100/50 p-2 border-b flex justify-between items-center px-4">
|
<div className="flex items-center justify-between border-b bg-slate-100/50 p-2 px-4">
|
||||||
<div className="flex items-center gap-2 text-xs text-slate-500 font-medium">
|
<div className="flex items-center gap-2 text-xs font-medium text-slate-500">
|
||||||
<FileText size={14} />
|
<FileText size={14} />
|
||||||
Plan_Estudios_ISC_2024.pdf
|
Plan_Estudios_ISC_2024.pdf
|
||||||
</div>
|
</div>
|
||||||
<Button variant="ghost" size="sm" className="text-xs gap-1 h-7">
|
<Button variant="ghost" size="sm" className="h-7 gap-1 text-xs">
|
||||||
Abrir en nueva pestaña <ExternalLink size={12} />
|
Abrir en nueva pestaña <ExternalLink size={12} />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<CardContent className="p-0 bg-slate-200/50 flex justify-center py-8 min-h-[800px]">
|
<CardContent className="flex min-h-[800px] justify-center bg-slate-200/50 p-0 py-8">
|
||||||
{/* SIMULACIÓN DE HOJA DE PAPEL */}
|
{/* SIMULACIÓN DE HOJA DE PAPEL */}
|
||||||
<div className="bg-white w-full max-w-[800px] shadow-2xl p-12 md:p-16 min-h-[1000px] border relative">
|
<div className="relative min-h-[1000px] w-full max-w-[800px] border bg-white p-12 shadow-2xl md:p-16">
|
||||||
|
|
||||||
{/* Contenido del Plan */}
|
{/* Contenido del Plan */}
|
||||||
<div className="text-center mb-12">
|
<div className="mb-12 text-center">
|
||||||
<p className="text-xs uppercase tracking-widest text-slate-400 font-bold mb-1">Universidad Tecnológica</p>
|
<p className="mb-1 text-xs font-bold tracking-widest text-slate-400 uppercase">
|
||||||
<h2 className="text-2xl font-bold text-slate-800">Plan de Estudios 2024</h2>
|
Universidad Tecnológica
|
||||||
<h3 className="text-lg text-teal-700 font-semibold">Ingeniería en Sistemas Computacionales</h3>
|
</p>
|
||||||
<p className="text-xs text-slate-500 mt-1">Facultad de Ingeniería</p>
|
<h2 className="text-2xl font-bold text-slate-800">
|
||||||
|
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="font-bold text-sm mb-2">1. Objetivo General</h4>
|
<h4 className="mb-2 text-sm font-bold">1. Objetivo General</h4>
|
||||||
<p className="text-sm leading-relaxed text-justify">
|
<p className="text-justify text-sm leading-relaxed">
|
||||||
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.
|
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.
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<h4 className="font-bold text-sm mb-2">2. Perfil de Ingreso</h4>
|
<h4 className="mb-2 text-sm font-bold">2. Perfil de Ingreso</h4>
|
||||||
<p className="text-sm leading-relaxed text-justify">
|
<p className="text-justify text-sm leading-relaxed">
|
||||||
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.
|
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.
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<h4 className="font-bold text-sm mb-2">3. Perfil de Egreso</h4>
|
<h4 className="mb-2 text-sm font-bold">3. Perfil de Egreso</h4>
|
||||||
<p className="text-sm leading-relaxed text-justify">
|
<p className="text-justify text-sm leading-relaxed">
|
||||||
Profesional capaz de diseñar, desarrollar e implementar sistemas de software de calidad, administrar infraestructuras de red y liderar proyectos tecnológicos multidisciplinarios.
|
Profesional capaz de diseñar, desarrollar e implementar
|
||||||
|
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 w-1 h-full bg-slate-100" />
|
<div className="absolute top-0 left-0 h-full w-1 bg-slate-100" />
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -114,18 +161,26 @@ function RouteComponent() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Componente pequeño para las tarjetas de estado superior
|
// 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 (
|
return (
|
||||||
<Card className="bg-white border-slate-200">
|
<Card className="border-slate-200 bg-white">
|
||||||
<CardContent className="p-4 flex items-center gap-4">
|
<CardContent className="flex items-center gap-4 p-4">
|
||||||
<div className="p-2 rounded-full bg-slate-50 border">
|
<div className="rounded-full border bg-slate-50 p-2">{icon}</div>
|
||||||
{icon}
|
|
||||||
</div>
|
|
||||||
<div>
|
<div>
|
||||||
<p className="text-[10px] uppercase font-bold text-slate-400 tracking-tight">{label}</p>
|
<p className="text-[10px] font-bold tracking-tight text-slate-400 uppercase">
|
||||||
|
{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>
|
||||||
</Card>
|
</Card>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,11 +12,12 @@ import { useState, useEffect } from 'react'
|
|||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import {
|
import {
|
||||||
ContextMenu,
|
DropdownMenu,
|
||||||
ContextMenuContent,
|
DropdownMenuContent,
|
||||||
ContextMenuItem,
|
DropdownMenuItem,
|
||||||
ContextMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from '@/components/ui/context-menu'
|
} from '@/components/ui/dropdown-menu'
|
||||||
|
import { Skeleton } from '@/components/ui/skeleton'
|
||||||
import { usePlan } from '@/data/hooks/usePlans'
|
import { usePlan } from '@/data/hooks/usePlans'
|
||||||
|
|
||||||
export const Route = createFileRoute('/planes/$planId/_detalle')({
|
export const Route = createFileRoute('/planes/$planId/_detalle')({
|
||||||
@@ -25,7 +26,7 @@ export const Route = createFileRoute('/planes/$planId/_detalle')({
|
|||||||
|
|
||||||
function RouteComponent() {
|
function RouteComponent() {
|
||||||
const { planId } = Route.useParams()
|
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
|
// Estados locales para manejar la edición "en vivo" antes de persistir
|
||||||
const [nombrePlan, setNombrePlan] = useState('')
|
const [nombrePlan, setNombrePlan] = useState('')
|
||||||
@@ -87,63 +88,86 @@ function RouteComponent() {
|
|||||||
|
|
||||||
<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 */}
|
||||||
<div className="flex flex-col items-start justify-between gap-4 md:flex-row">
|
{isLoading ? (
|
||||||
<div>
|
/* ===== SKELETON ===== */
|
||||||
<h1 className="flex items-baseline gap-2 text-3xl font-bold tracking-tight text-slate-900">
|
<div className="mx-auto max-w-[1600px] p-8">
|
||||||
<span>{nivelPlan} en</span>
|
<div className="grid grid-cols-1 gap-6 lg:grid-cols-2">
|
||||||
<span
|
{Array.from({ length: 6 }).map((_, i) => (
|
||||||
role="textbox"
|
<DatosGeneralesSkeleton key={i} />
|
||||||
tabIndex={0}
|
))}
|
||||||
contentEditable
|
</div>
|
||||||
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>
|
|
||||||
<p className="mt-1 text-lg font-medium text-slate-500">
|
|
||||||
{data?.carreras?.facultades?.nombre}{' '}
|
|
||||||
{data?.carreras?.nombre_corto}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className="flex flex-col items-start justify-between gap-4 md:flex-row">
|
||||||
|
<div>
|
||||||
|
<h1 className="flex items-baseline gap-2 text-3xl font-bold tracking-tight text-slate-900">
|
||||||
|
<span>{nivelPlan} en</span>
|
||||||
|
<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>
|
||||||
|
<p className="mt-1 text-lg font-medium text-slate-500">
|
||||||
|
{data?.carreras?.facultades?.nombre}{' '}
|
||||||
|
{data?.carreras?.nombre_corto}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<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">
|
{/* <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}
|
<CheckCircle2 size={12} /> {data?.estados_plan?.etiqueta}
|
||||||
</Badge> */}
|
</Badge> */}
|
||||||
<Badge
|
<Badge
|
||||||
className={`gap-1 border-teal-200 bg-teal-50 px-3 text-teal-700 hover:bg-teal-100`}
|
className={`gap-1 border-teal-200 bg-teal-50 px-3 text-teal-700 hover:bg-teal-100`}
|
||||||
>
|
>
|
||||||
{data?.estados_plan?.etiqueta}
|
{data?.estados_plan?.etiqueta}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* 3. Cards de Información con Context Menu */}
|
{/* 3. Cards de Información con Context Menu */}
|
||||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-4">
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-4">
|
||||||
<ContextMenu>
|
<DropdownMenu>
|
||||||
<ContextMenuTrigger>
|
<DropdownMenuTrigger>
|
||||||
{/* Eliminamos el div extra y aplicamos el estilo directamente al trigger si es necesario,
|
|
||||||
pero con asChild, la InfoCard será el trigger real */}
|
|
||||||
<InfoCard
|
<InfoCard
|
||||||
icon={<GraduationCap className="text-slate-400" />}
|
icon={<GraduationCap className="text-slate-400" />}
|
||||||
label="Nivel"
|
label="Nivel"
|
||||||
value={nivelPlan}
|
value={nivelPlan}
|
||||||
isEditable
|
isEditable
|
||||||
/>
|
/>
|
||||||
</ContextMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<ContextMenuContent className="w-48">
|
|
||||||
|
<DropdownMenuContent className="w-48">
|
||||||
{niveles.map((n) => (
|
{niveles.map((n) => (
|
||||||
<ContextMenuItem key={n} onClick={() => setNivelPlan(n)}>
|
<DropdownMenuItem
|
||||||
|
key={n}
|
||||||
|
onClick={() => {
|
||||||
|
setNivelPlan(n)
|
||||||
|
setIsDirty(true)
|
||||||
|
}}
|
||||||
|
>
|
||||||
{n}
|
{n}
|
||||||
</ContextMenuItem>
|
</DropdownMenuItem>
|
||||||
))}
|
))}
|
||||||
</ContextMenuContent>
|
</DropdownMenuContent>
|
||||||
</ContextMenu>
|
</DropdownMenu>
|
||||||
|
|
||||||
<InfoCard
|
<InfoCard
|
||||||
icon={<Clock className="text-slate-400" />}
|
icon={<Clock className="text-slate-400" />}
|
||||||
@@ -212,7 +236,7 @@ function InfoCard({
|
|||||||
<div
|
<div
|
||||||
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 ${
|
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 ${
|
||||||
isEditable
|
isEditable
|
||||||
? 'cursor-context-menu hover:border-teal-200 hover:bg-white'
|
? 'cursor-pointer hover:border-teal-200 hover:bg-white focus:outline-none focus-visible:ring-2 focus-visible:ring-teal-500/40'
|
||||||
: ''
|
: ''
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
@@ -253,3 +277,23 @@ function Tab({
|
|||||||
</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/datos',
|
to: '/planes/$planId/materias',
|
||||||
params,
|
params,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user