From 2abe296b9e189904e448917876797af9798fe01a Mon Sep 17 00:00:00 2001 From: Guillermo Arrieta Medina Date: Fri, 20 Mar 2026 17:44:36 -0600 Subject: [PATCH] close #200: Se guardan los docx y pdf con el nombre del plan/asignatura --- .../planes/$planId/_detalle/documento.tsx | 43 ++++++++++++++---- .../asignaturas/$asignaturaId/documento.tsx | 45 +++++++++++++++---- 2 files changed, 70 insertions(+), 18 deletions(-) diff --git a/src/routes/planes/$planId/_detalle/documento.tsx b/src/routes/planes/$planId/_detalle/documento.tsx index 2df3f2a..2c2bdbf 100644 --- a/src/routes/planes/$planId/_detalle/documento.tsx +++ b/src/routes/planes/$planId/_detalle/documento.tsx @@ -12,6 +12,7 @@ import { useCallback, useEffect, useRef, useState } from 'react' import { Button } from '@/components/ui/button' import { Card, CardContent } from '@/components/ui/card' +import { usePlan } from '@/data' import { fetchPlanPdf } from '@/data/api/document.api' export const Route = createFileRoute('/planes/$planId/_detalle/documento')({ @@ -20,34 +21,40 @@ export const Route = createFileRoute('/planes/$planId/_detalle/documento')({ function RouteComponent() { const { planId } = useParams({ from: '/planes/$planId/_detalle/documento' }) + const { data: plan } = usePlan(planId) const [pdfUrl, setPdfUrl] = useState(null) const pdfUrlRef = useRef(null) + const isMountedRef = useRef(false) const [isLoading, setIsLoading] = useState(true) + const planFileBaseName = sanitizeFileBaseName(plan?.nombre ?? 'plan_estudios') + const loadPdfPreview = useCallback(async () => { try { - setIsLoading(true) + if (isMountedRef.current) setIsLoading(true) const pdfBlob = await fetchPlanPdf({ plan_estudio_id: planId, convertTo: 'pdf', }) + + if (!isMountedRef.current) return const url = window.URL.createObjectURL(pdfBlob) - setPdfUrl((prev) => { - if (prev) window.URL.revokeObjectURL(prev) - pdfUrlRef.current = url - return url - }) + if (pdfUrlRef.current) window.URL.revokeObjectURL(pdfUrlRef.current) + pdfUrlRef.current = url + setPdfUrl(url) } catch (error) { console.error('Error cargando preview:', error) } finally { - setIsLoading(false) + if (isMountedRef.current) setIsLoading(false) } }, [planId]) useEffect(() => { + isMountedRef.current = true loadPdfPreview() return () => { + isMountedRef.current = false if (pdfUrlRef.current) window.URL.revokeObjectURL(pdfUrlRef.current) } }, [loadPdfPreview]) @@ -62,7 +69,7 @@ function RouteComponent() { const url = window.URL.createObjectURL(pdfBlob) const link = document.createElement('a') link.href = url - link.download = 'plan_estudios.pdf' + link.download = `${planFileBaseName}.pdf` document.body.appendChild(link) link.click() @@ -83,7 +90,7 @@ function RouteComponent() { const url = window.URL.createObjectURL(docBlob) const link = document.createElement('a') link.href = url - link.download = 'plan_estudios.docx' + link.download = `${planFileBaseName}.docx` document.body.appendChild(link) link.click() @@ -195,6 +202,24 @@ function RouteComponent() { ) } +function sanitizeFileBaseName(input: string): string { + const text = String(input) + const withoutControlChars = Array.from(text) + .filter((ch) => { + const code = ch.charCodeAt(0) + return code >= 32 && code !== 127 + }) + .join('') + + const cleaned = withoutControlChars + .replace(/[<>:"/\\|?*]+/g, ' ') + .replace(/\s+/g, ' ') + .trim() + .replace(/[. ]+$/g, '') + + return (cleaned || 'documento').slice(0, 150) +} + // Componente pequeño para las tarjetas de estado superior function StatusCard({ icon, diff --git a/src/routes/planes/$planId/asignaturas/$asignaturaId/documento.tsx b/src/routes/planes/$planId/asignaturas/$asignaturaId/documento.tsx index 285a8e4..5bd31bc 100644 --- a/src/routes/planes/$planId/asignaturas/$asignaturaId/documento.tsx +++ b/src/routes/planes/$planId/asignaturas/$asignaturaId/documento.tsx @@ -2,6 +2,7 @@ import { createFileRoute, useParams } from '@tanstack/react-router' import { useCallback, useEffect, useRef, useState } from 'react' import { DocumentoSEPTab } from '@/components/asignaturas/detalle/DocumentoSEPTab' +import { useSubject } from '@/data' import { fetchAsignaturaPdf } from '@/data/api/document.api' export const Route = createFileRoute( @@ -15,38 +16,46 @@ function RouteComponent() { from: '/planes/$planId/asignaturas/$asignaturaId/documento', }) + const { data: asignatura } = useSubject(asignaturaId) + const asignaturaFileBaseName = sanitizeFileBaseName( + asignatura?.nombre ?? 'documento_sep', + ) + const [pdfUrl, setPdfUrl] = useState(null) const pdfUrlRef = useRef(null) + const isMountedRef = useRef(false) const [isLoading, setIsLoading] = useState(true) const [isRegenerating, setIsRegenerating] = useState(false) const loadPdfPreview = useCallback(async () => { try { - setIsLoading(true) + if (isMountedRef.current) setIsLoading(true) const pdfBlob = await fetchAsignaturaPdf({ asignatura_id: asignaturaId, convertTo: 'pdf', }) + if (!isMountedRef.current) return + const url = window.URL.createObjectURL(pdfBlob) - setPdfUrl((prev) => { - if (prev) window.URL.revokeObjectURL(prev) - pdfUrlRef.current = url - return url - }) + if (pdfUrlRef.current) window.URL.revokeObjectURL(pdfUrlRef.current) + pdfUrlRef.current = url + setPdfUrl(url) } catch (error) { console.error('Error cargando PDF:', error) } finally { - setIsLoading(false) + if (isMountedRef.current) setIsLoading(false) } }, [asignaturaId]) useEffect(() => { + isMountedRef.current = true loadPdfPreview() return () => { + isMountedRef.current = false if (pdfUrlRef.current) window.URL.revokeObjectURL(pdfUrlRef.current) } }, [loadPdfPreview]) @@ -60,7 +69,7 @@ function RouteComponent() { const url = window.URL.createObjectURL(pdfBlob) const link = document.createElement('a') link.href = url - link.download = 'documento_sep.pdf' + link.download = `${asignaturaFileBaseName}.pdf` document.body.appendChild(link) link.click() link.remove() @@ -75,7 +84,7 @@ function RouteComponent() { const url = window.URL.createObjectURL(docBlob) const link = document.createElement('a') link.href = url - link.download = 'documento_sep.docx' + link.download = `${asignaturaFileBaseName}.docx` document.body.appendChild(link) link.click() link.remove() @@ -103,3 +112,21 @@ function RouteComponent() { /> ) } + +function sanitizeFileBaseName(input: string): string { + const text = String(input) + const withoutControlChars = Array.from(text) + .filter((ch) => { + const code = ch.charCodeAt(0) + return code >= 32 && code !== 127 + }) + .join('') + + const cleaned = withoutControlChars + .replace(/[<>:"/\\|?*]+/g, ' ') + .replace(/\s+/g, ' ') + .trim() + .replace(/[. ]+$/g, '') + + return (cleaned || 'documento').slice(0, 150) +}