190 lines
5.7 KiB
TypeScript
190 lines
5.7 KiB
TypeScript
import { createFileRoute, useParams } from '@tanstack/react-router'
|
|
import {
|
|
FileText,
|
|
Download,
|
|
RefreshCcw,
|
|
ExternalLink,
|
|
CheckCircle2,
|
|
Clock,
|
|
FileJson,
|
|
} from 'lucide-react'
|
|
import { useCallback, useEffect, useState } from '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 [pdfUrl, setPdfUrl] = useState<string | null>(null)
|
|
const [isLoading, setIsLoading] = useState(true)
|
|
|
|
const loadPdfPreview = useCallback(async () => {
|
|
try {
|
|
setIsLoading(true)
|
|
const pdfBlob = await fetchPlanPdf({ plan_estudio_id: planId })
|
|
const url = window.URL.createObjectURL(pdfBlob)
|
|
|
|
// Limpiar URL anterior si existe para evitar fugas de memoria
|
|
if (pdfUrl) window.URL.revokeObjectURL(pdfUrl)
|
|
|
|
setPdfUrl(url)
|
|
} catch (error) {
|
|
console.error('Error cargando preview:', error)
|
|
} finally {
|
|
setIsLoading(false)
|
|
}
|
|
}, [planId])
|
|
|
|
useEffect(() => {
|
|
loadPdfPreview()
|
|
return () => {
|
|
if (pdfUrl) window.URL.revokeObjectURL(pdfUrl)
|
|
}
|
|
}, [loadPdfPreview])
|
|
|
|
const handleDownloadPdf = async () => {
|
|
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 (
|
|
<div className="flex min-h-screen flex-col gap-6 bg-slate-50/30 p-6">
|
|
{/* HEADER DE ACCIONES */}
|
|
<div className="flex flex-col items-start justify-between gap-4 md:flex-row md:items-center">
|
|
<div>
|
|
<h1 className="text-xl font-bold text-slate-800">
|
|
Documento del Plan
|
|
</h1>
|
|
<p className="text-muted-foreground text-sm">
|
|
Vista previa y descarga del documento oficial
|
|
</p>
|
|
</div>
|
|
<div className="flex gap-2">
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
className="gap-2"
|
|
onClick={loadPdfPreview}
|
|
>
|
|
<RefreshCcw size={16} /> Regenerar
|
|
</Button>
|
|
<Button variant="outline" size="sm" className="gap-2">
|
|
<Download size={16} /> Descargar Word
|
|
</Button>
|
|
<Button
|
|
size="sm"
|
|
className="gap-2 bg-teal-700 hover:bg-teal-800"
|
|
onClick={handleDownloadPdf}
|
|
>
|
|
<Download size={16} /> Descargar PDF
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* TARJETAS DE ESTADO */}
|
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
|
|
<StatusCard
|
|
icon={<CheckCircle2 className="text-green-500" />}
|
|
label="Estado"
|
|
value="Generado"
|
|
/>
|
|
<StatusCard
|
|
icon={<Clock className="text-blue-500" />}
|
|
label="Última generación"
|
|
value="28 Ene 2024, 11:30"
|
|
/>
|
|
<StatusCard
|
|
icon={<FileJson className="text-orange-500" />}
|
|
label="Versión"
|
|
value="v1.2"
|
|
/>
|
|
</div>
|
|
|
|
{/* CONTENEDOR DEL DOCUMENTO (Visor) */}
|
|
{/* CONTENEDOR DEL VISOR REAL */}
|
|
<Card className="overflow-hidden border-slate-200 shadow-sm">
|
|
<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 font-medium text-slate-500">
|
|
<FileText size={14} /> Preview_Documento.pdf
|
|
</div>
|
|
{pdfUrl && (
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
className="h-7 gap-1 text-xs"
|
|
onClick={() => window.open(pdfUrl, '_blank')}
|
|
>
|
|
Abrir en nueva pestaña <ExternalLink size={12} />
|
|
</Button>
|
|
)}
|
|
</div>
|
|
|
|
<CardContent className="flex min-h-[800px] justify-center bg-slate-500 p-0">
|
|
{isLoading ? (
|
|
<div className="flex flex-col items-center justify-center gap-4 text-white">
|
|
<RefreshCcw size={40} className="animate-spin opacity-50" />
|
|
<p className="animate-pulse">Generando vista previa del PDF...</p>
|
|
</div>
|
|
) : pdfUrl ? (
|
|
/* 3. VISOR DE PDF REAL */
|
|
<iframe
|
|
src={`${pdfUrl}#toolbar=0&navpanes=0`}
|
|
className="h-[1000px] w-full max-w-[1000px] border-none shadow-2xl"
|
|
title="PDF Preview"
|
|
/>
|
|
) : (
|
|
<div className="flex items-center justify-center p-20 text-slate-400">
|
|
No se pudo cargar la vista previa.
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
// Componente pequeño para las tarjetas de estado superior
|
|
function StatusCard({
|
|
icon,
|
|
label,
|
|
value,
|
|
}: {
|
|
icon: React.ReactNode
|
|
label: string
|
|
value: string
|
|
}) {
|
|
return (
|
|
<Card className="border-slate-200 bg-white">
|
|
<CardContent className="flex items-center gap-4 p-4">
|
|
<div className="rounded-full border bg-slate-50 p-2">{icon}</div>
|
|
<div>
|
|
<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>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
)
|
|
}
|