Frontend v1.0.80:
El botón "📄 Exportar PDF" ahora solo es visible para admin y asesor Los mecánicos (role === 'mechanic') pueden ver el modal de inspección pero NO pueden exportar el PDF Backend v1.0.82 (sin cambios adicionales) Resumen de permisos: ✅ Admin: Ver inspección + Exportar PDF + Ver historial + Inactivar ✅ Asesor: Ver inspección + Exportar PDF ❌ Mecánico: Solo ver inspección + Continuar incompletas
This commit is contained in:
@@ -1428,15 +1428,14 @@ def generate_inspection_pdf(inspection_id: int, db: Session) -> str:
|
|||||||
# ===== PORTADA =====
|
# ===== PORTADA =====
|
||||||
elements.append(Spacer(1, 10*mm))
|
elements.append(Spacer(1, 10*mm))
|
||||||
|
|
||||||
# Función helper para cargar y dimensionar logos
|
# Función helper para cargar y dimensionar logos (optimizada)
|
||||||
def load_logo(logo_url, max_width_mm=45, max_height_mm=35):
|
def load_logo(logo_url, max_width_mm=45, max_height_mm=35):
|
||||||
"""Carga un logo desde URL y retorna objeto Image con dimensiones ajustadas"""
|
"""Carga un logo desde URL y retorna objeto Image con dimensiones ajustadas"""
|
||||||
if not logo_url:
|
if not logo_url:
|
||||||
return None
|
return None
|
||||||
try:
|
try:
|
||||||
print(f"🔍 Cargando logo desde: {logo_url}")
|
# Reducir timeout para respuestas más rápidas
|
||||||
logo_resp = requests.get(logo_url, timeout=10)
|
logo_resp = requests.get(logo_url, timeout=5)
|
||||||
print(f"📡 Respuesta: {logo_resp.status_code}")
|
|
||||||
|
|
||||||
if logo_resp.status_code == 200:
|
if logo_resp.status_code == 200:
|
||||||
logo_bytes = BytesIO(logo_resp.content)
|
logo_bytes = BytesIO(logo_resp.content)
|
||||||
@@ -1455,21 +1454,42 @@ def generate_inspection_pdf(inspection_id: int, db: Session) -> str:
|
|||||||
logo_img.drawWidth = logo_width
|
logo_img.drawWidth = logo_width
|
||||||
logo_img.drawHeight = logo_height
|
logo_img.drawHeight = logo_height
|
||||||
|
|
||||||
print(f"✅ Logo cargado ({logo_width/mm:.1f}mm x {logo_height/mm:.1f}mm)")
|
|
||||||
return logo_img
|
return logo_img
|
||||||
else:
|
else:
|
||||||
print(f"❌ Error HTTP: {logo_resp.status_code}")
|
print(f"❌ Error HTTP cargando logo: {logo_resp.status_code}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"⚠️ Error cargando logo: {e}")
|
print(f"⚠️ Error cargando logo: {str(e)[:100]}")
|
||||||
import traceback
|
|
||||||
traceback.print_exc()
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Cargar ambos logos
|
# Cargar ambos logos en paralelo usando ThreadPoolExecutor
|
||||||
company_logo = load_logo(company_logo_url, max_width_mm=50, max_height_mm=35)
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||||
checklist_logo = load_logo(checklist_logo_url, max_width_mm=50, max_height_mm=35)
|
|
||||||
|
|
||||||
# Crear tabla con logos en los extremos
|
company_logo = None
|
||||||
|
checklist_logo = None
|
||||||
|
|
||||||
|
with ThreadPoolExecutor(max_workers=2) as executor:
|
||||||
|
futures = {}
|
||||||
|
if company_logo_url:
|
||||||
|
futures[executor.submit(load_logo, company_logo_url, 50, 35)] = 'company'
|
||||||
|
if checklist_logo_url:
|
||||||
|
futures[executor.submit(load_logo, checklist_logo_url, 50, 35)] = 'checklist'
|
||||||
|
|
||||||
|
for future in as_completed(futures):
|
||||||
|
logo_type = futures[future]
|
||||||
|
try:
|
||||||
|
result = future.result()
|
||||||
|
if logo_type == 'company':
|
||||||
|
company_logo = result
|
||||||
|
if result:
|
||||||
|
print(f"✅ Logo empresa cargado")
|
||||||
|
elif logo_type == 'checklist':
|
||||||
|
checklist_logo = result
|
||||||
|
if result:
|
||||||
|
print(f"✅ Logo checklist cargado")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Error procesando logo {logo_type}: {e}")
|
||||||
|
|
||||||
|
# Crear tabla con logos en los extremos (ancho total disponible ~180mm)
|
||||||
logo_row = []
|
logo_row = []
|
||||||
|
|
||||||
# Logo empresa (izquierda)
|
# Logo empresa (izquierda)
|
||||||
@@ -1478,7 +1498,7 @@ def generate_inspection_pdf(inspection_id: int, db: Session) -> str:
|
|||||||
else:
|
else:
|
||||||
logo_row.append(Paragraph("", styles['Normal'])) # Espacio vacío
|
logo_row.append(Paragraph("", styles['Normal'])) # Espacio vacío
|
||||||
|
|
||||||
# Espaciador central
|
# Espaciador central flexible
|
||||||
logo_row.append(Paragraph("", styles['Normal']))
|
logo_row.append(Paragraph("", styles['Normal']))
|
||||||
|
|
||||||
# Logo checklist (derecha)
|
# Logo checklist (derecha)
|
||||||
@@ -1487,13 +1507,16 @@ def generate_inspection_pdf(inspection_id: int, db: Session) -> str:
|
|||||||
else:
|
else:
|
||||||
logo_row.append(Paragraph("", styles['Normal'])) # Espacio vacío
|
logo_row.append(Paragraph("", styles['Normal'])) # Espacio vacío
|
||||||
|
|
||||||
# Crear tabla con logos
|
# Crear tabla con logos - columnas ajustadas para maximizar separación
|
||||||
logo_table = Table([logo_row], colWidths=[60*mm, 60*mm, 60*mm])
|
# Columna 1: 55mm (logo empresa), Columna 2: 70mm (espacio), Columna 3: 55mm (logo checklist)
|
||||||
|
logo_table = Table([logo_row], colWidths=[55*mm, 70*mm, 55*mm])
|
||||||
logo_table.setStyle(TableStyle([
|
logo_table.setStyle(TableStyle([
|
||||||
('ALIGN', (0, 0), (0, 0), 'LEFT'), # Logo empresa a la izquierda
|
('ALIGN', (0, 0), (0, 0), 'LEFT'), # Logo empresa a la izquierda
|
||||||
('ALIGN', (1, 0), (1, 0), 'CENTER'), # Centro vacío
|
('ALIGN', (1, 0), (1, 0), 'CENTER'), # Centro vacío
|
||||||
('ALIGN', (2, 0), (2, 0), 'RIGHT'), # Logo checklist a la derecha
|
('ALIGN', (2, 0), (2, 0), 'RIGHT'), # Logo checklist a la derecha
|
||||||
('VALIGN', (0, 0), (-1, -1), 'TOP'),
|
('VALIGN', (0, 0), (-1, -1), 'MIDDLE'), # Alineación vertical al centro
|
||||||
|
# DEBUG: Agregar bordes para ver la distribución
|
||||||
|
# ('GRID', (0, 0), (-1, -1), 0.5, colors.red),
|
||||||
]))
|
]))
|
||||||
elements.append(logo_table)
|
elements.append(logo_table)
|
||||||
elements.append(Spacer(1, 5*mm))
|
elements.append(Spacer(1, 5*mm))
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "checklist-frontend",
|
"name": "checklist-frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.0.79",
|
"version": "1.0.80",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -3479,60 +3479,63 @@ function InspectionDetailModal({ inspection, user, onClose, onUpdate, onContinue
|
|||||||
{loadingAudit ? 'Cargando...' : 'Ver Historial de Cambios'}
|
{loadingAudit ? 'Cargando...' : 'Ver Historial de Cambios'}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<button
|
{/* Botón Exportar PDF - solo para admin y asesor */}
|
||||||
onClick={async () => {
|
{(user?.role === 'admin' || user?.role === 'asesor') && (
|
||||||
try {
|
<button
|
||||||
const token = localStorage.getItem('token')
|
onClick={async () => {
|
||||||
const API_URL = import.meta.env.VITE_API_URL || ''
|
try {
|
||||||
const response = await fetch(`${API_URL}/api/inspections/${inspection.id}/pdf`, {
|
const token = localStorage.getItem('token')
|
||||||
headers: { 'Authorization': `Bearer ${token}` }
|
const API_URL = import.meta.env.VITE_API_URL || ''
|
||||||
})
|
const response = await fetch(`${API_URL}/api/inspections/${inspection.id}/pdf`, {
|
||||||
if (response.ok) {
|
headers: { 'Authorization': `Bearer ${token}` }
|
||||||
const contentType = response.headers.get('content-type')
|
})
|
||||||
if (contentType && contentType.includes('application/pdf')) {
|
if (response.ok) {
|
||||||
const blob = await response.blob()
|
const contentType = response.headers.get('content-type')
|
||||||
const url = window.URL.createObjectURL(blob)
|
if (contentType && contentType.includes('application/pdf')) {
|
||||||
const a = document.createElement('a')
|
const blob = await response.blob()
|
||||||
a.href = url
|
const url = window.URL.createObjectURL(blob)
|
||||||
a.download = `inspeccion_${inspection.id}_${inspection.vehicle_plate || 'sin-patente'}.pdf`
|
const a = document.createElement('a')
|
||||||
document.body.appendChild(a)
|
a.href = url
|
||||||
a.click()
|
a.download = `inspeccion_${inspection.id}_${inspection.vehicle_plate || 'sin-patente'}.pdf`
|
||||||
document.body.removeChild(a)
|
document.body.appendChild(a)
|
||||||
window.URL.revokeObjectURL(url)
|
a.click()
|
||||||
} else {
|
document.body.removeChild(a)
|
||||||
const data = await response.json()
|
window.URL.revokeObjectURL(url)
|
||||||
if (data.pdf_url) {
|
|
||||||
const pdfRes = await fetch(data.pdf_url)
|
|
||||||
if (pdfRes.ok) {
|
|
||||||
const pdfBlob = await pdfRes.blob()
|
|
||||||
const pdfUrl = window.URL.createObjectURL(pdfBlob)
|
|
||||||
const a = document.createElement('a')
|
|
||||||
a.href = pdfUrl
|
|
||||||
a.download = `inspeccion_${inspection.id}_${inspection.vehicle_plate || 'sin-patente'}.pdf`
|
|
||||||
document.body.appendChild(a)
|
|
||||||
a.click()
|
|
||||||
document.body.removeChild(a)
|
|
||||||
window.URL.revokeObjectURL(pdfUrl)
|
|
||||||
} else {
|
|
||||||
alert('No se pudo descargar el PDF desde MinIO')
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
alert('No se encontró la URL del PDF')
|
const data = await response.json()
|
||||||
|
if (data.pdf_url) {
|
||||||
|
const pdfRes = await fetch(data.pdf_url)
|
||||||
|
if (pdfRes.ok) {
|
||||||
|
const pdfBlob = await pdfRes.blob()
|
||||||
|
const pdfUrl = window.URL.createObjectURL(pdfBlob)
|
||||||
|
const a = document.createElement('a')
|
||||||
|
a.href = pdfUrl
|
||||||
|
a.download = `inspeccion_${inspection.id}_${inspection.vehicle_plate || 'sin-patente'}.pdf`
|
||||||
|
document.body.appendChild(a)
|
||||||
|
a.click()
|
||||||
|
document.body.removeChild(a)
|
||||||
|
window.URL.revokeObjectURL(pdfUrl)
|
||||||
|
} else {
|
||||||
|
alert('No se pudo descargar el PDF desde MinIO')
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
alert('No se encontró la URL del PDF')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
alert('Error al generar PDF')
|
||||||
}
|
}
|
||||||
} else {
|
} catch (error) {
|
||||||
|
console.error('Error:', error)
|
||||||
alert('Error al generar PDF')
|
alert('Error al generar PDF')
|
||||||
}
|
}
|
||||||
} catch (error) {
|
}}
|
||||||
console.error('Error:', error)
|
className="px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition flex items-center gap-2"
|
||||||
alert('Error al generar PDF')
|
>
|
||||||
}
|
<span>📄</span>
|
||||||
}}
|
Exportar PDF
|
||||||
className="px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition flex items-center gap-2"
|
</button>
|
||||||
>
|
)}
|
||||||
<span>📄</span>
|
|
||||||
Exportar PDF
|
|
||||||
</button>
|
|
||||||
{user?.role === 'admin' && (
|
{user?.role === 'admin' && (
|
||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
|
|||||||
Reference in New Issue
Block a user