175 lines
7.3 KiB
TypeScript
175 lines
7.3 KiB
TypeScript
import { useEffect, useState } from "react"
|
|
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"
|
|
import { Badge } from "@/components/ui/badge"
|
|
import { Label } from "@/components/ui/label"
|
|
import { Button } from "@/components/ui/button"
|
|
import * as Icons from "lucide-react"
|
|
import type { RefRow } from "@/types/RefRow"
|
|
|
|
// POST -> recibe blob PDF y (opcional) Content-Disposition
|
|
async function fetchPdfBlob(url: string, body: { documentos_id: string }) {
|
|
const res = await fetch(url, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(body),
|
|
})
|
|
if (!res.ok) throw new Error(await res.text())
|
|
const blob = await res.blob()
|
|
return { blob, disposition: res.headers.get("content-disposition") ?? undefined }
|
|
}
|
|
|
|
function filenameFromDisposition(disposition?: string, fallback = "archivo.pdf") {
|
|
if (!disposition) return fallback
|
|
const star = /filename\*\=UTF-8''([^;]+)/i.exec(disposition)?.[1]
|
|
if (star) return decodeURIComponent(star)
|
|
const simple = /filename\=\"?([^\";]+)\"?/i.exec(disposition)?.[1]
|
|
return simple ?? fallback
|
|
}
|
|
|
|
export function DetailDialog({
|
|
row,
|
|
onClose,
|
|
pdfUrl = "/api/get/documento", // ← permite inyectar el endpoint
|
|
}: {
|
|
row: RefRow | null
|
|
onClose: () => void
|
|
pdfUrl?: string
|
|
}) {
|
|
console.log("DetailDialog render", { row })
|
|
const [viewerUrl, setViewerUrl] = useState<string | null>(null)
|
|
const [currentBlob, setCurrentBlob] = useState<Blob | null>(null)
|
|
const [filename, setFilename] = useState<string>("archivo.pdf")
|
|
const open = !!row
|
|
|
|
useEffect(() => {
|
|
if (!open) return
|
|
|
|
let revoked = false
|
|
let urlToRevoke: string | null = null
|
|
const ctrl = new AbortController()
|
|
|
|
async function load() {
|
|
console.log(row)
|
|
if (!row?.documentos_id) {
|
|
setViewerUrl(null)
|
|
setCurrentBlob(null)
|
|
console.warn("No hay documentos_id en el row")
|
|
return
|
|
}
|
|
try {
|
|
const { blob, disposition } = await fetchPdfBlob(`${import.meta.env.VITE_BACK_ORIGIN}/api/get/documento`, { documentos_id: row.documentos_id })
|
|
if (ctrl.signal.aborted) return
|
|
const name = row.titulo_archivo ? `${row.titulo_archivo}.pdf` : filenameFromDisposition(disposition)
|
|
setFilename(name)
|
|
const url = URL.createObjectURL(blob)
|
|
urlToRevoke = url
|
|
if (!revoked) {
|
|
setViewerUrl(url)
|
|
setCurrentBlob(blob)
|
|
}
|
|
} catch (e) {
|
|
console.error("Carga de PDF falló:", e)
|
|
setViewerUrl(null)
|
|
setCurrentBlob(null)
|
|
}
|
|
}
|
|
|
|
load()
|
|
|
|
return () => {
|
|
revoked = true
|
|
ctrl.abort()
|
|
if (urlToRevoke) URL.revokeObjectURL(urlToRevoke)
|
|
}
|
|
}, [open, row?.s3_file_path, row?.titulo_archivo, pdfUrl])
|
|
|
|
async function downloadFile() {
|
|
try {
|
|
// Si ya tenemos el blob, úsalo
|
|
if (currentBlob) {
|
|
const link = document.createElement("a")
|
|
const href = URL.createObjectURL(currentBlob)
|
|
link.href = href
|
|
link.download = filename
|
|
link.click()
|
|
URL.revokeObjectURL(href)
|
|
return
|
|
}
|
|
|
|
// Si no, vuelve a pedirlo (p. ej., si el user abre y descarga sin render previo)
|
|
if (!row?.documentos_id) throw new Error("No hay contenido para descargar.")
|
|
const { blob, disposition } = await fetchPdfBlob(`${import.meta.env.VITE_BACK_ORIGIN}/api/get/documento`, { documentos_id: row.documentos_id })
|
|
const name = row.titulo_archivo ? `${row.titulo_archivo}.pdf` : filenameFromDisposition(disposition)
|
|
const link = document.createElement("a")
|
|
const href = URL.createObjectURL(blob)
|
|
link.href = href
|
|
link.download = name
|
|
link.click()
|
|
URL.revokeObjectURL(href)
|
|
} catch (error) {
|
|
console.error("Error al descargar el archivo:", error)
|
|
alert("No se pudo descargar el archivo.")
|
|
}
|
|
}
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={(o) => !o && onClose()}>
|
|
<DialogContent className="max-w-fit">
|
|
<DialogHeader>
|
|
<DialogTitle className="font-mono">{row?.titulo_archivo ?? "(Sin título)"}</DialogTitle>
|
|
<DialogDescription>{row?.descripcion || "Sin descripción"}</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
{row && (
|
|
<div className="space-y-3">
|
|
<div className="text-xs flex flex-wrap gap-2">
|
|
<Badge variant="outline">{row.tipo_contenido ?? "—"}</Badge>
|
|
<Badge variant="outline">{row.interno ? "Interno" : "Externo"}</Badge>
|
|
<Badge variant="outline">{row.procesado ? "Procesado" : "Pendiente"}</Badge>
|
|
{row.fuente_autoridad && <Badge variant="outline">{row.fuente_autoridad}</Badge>}
|
|
{row.fecha_subida && (
|
|
<span className="inline-flex items-center gap-1">
|
|
<Icons.CalendarClock className="w-3 h-3" />
|
|
{new Date(row.fecha_subida).toLocaleString()}
|
|
</span>
|
|
)}
|
|
</div>
|
|
{/* Tags
|
|
{row.tags?.length ? (
|
|
<div className="text-xs text-neutral-600">
|
|
<span className="font-medium">Tags: </span>
|
|
{row.tags.join(", ")}
|
|
</div>
|
|
) : null} */}
|
|
|
|
<div>
|
|
<Label className="text-xs text-neutral-600">Instrucciones</Label>
|
|
<div className="mt-1 rounded-xl border bg-white/60 p-3 text-sm whitespace-pre-wrap">
|
|
{row.instrucciones || "—"}
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<Label className="text-xs text-neutral-600">Contenido (PDF)</Label>
|
|
{viewerUrl ? (
|
|
<iframe
|
|
className="mt-1 rounded-xl border bg-neutral-950 text-neutral-100 w-full h-[360px]"
|
|
src={viewerUrl}
|
|
title="PDF Viewer"
|
|
/>
|
|
) : (
|
|
<div className="mt-1 rounded-xl border p-3 text-sm text-neutral-600">No se pudo cargar el PDF.</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<DialogFooter>
|
|
<Button variant="outline" onClick={downloadFile}>Descargar</Button>
|
|
<Button onClick={onClose}>Cerrar</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
)
|
|
}
|