diff --git a/backend/app/main.py b/backend/app/main.py index a990a31..b5d3d30 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -1668,126 +1668,32 @@ def export_inspection_to_pdf( current_user: models.User = Depends(get_current_user), db: Session = Depends(get_db) ): - """Exportar inspección a PDF con imágenes""" + """Descargar el PDF guardado en MinIO para la inspección""" from fastapi.responses import StreamingResponse - from reportlab.lib.pagesizes import letter, A4 - from reportlab.lib import colors - from reportlab.lib.units import inch - from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer, Image as RLImage, PageBreak - from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle - from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_RIGHT - from io import BytesIO - import base64 - + import requests # Obtener inspección inspection = db.query(models.Inspection).filter( models.Inspection.id == inspection_id ).first() - if not inspection: raise HTTPException(status_code=404, detail="Inspección no encontrada") - - # Verificar permisos (admin, asesor o mecánico dueño) if current_user.role not in ["admin", "asesor"] and inspection.mechanic_id != current_user.id: raise HTTPException(status_code=403, detail="No tienes permisos para ver esta inspección") - - # Obtener datos relacionados - checklist = db.query(models.Checklist).filter(models.Checklist.id == inspection.checklist_id).first() - mechanic = db.query(models.User).filter(models.User.id == inspection.mechanic_id).first() - answers = db.query(models.Answer).options( - joinedload(models.Answer.media_files) - ).join(models.Question).filter( - models.Answer.inspection_id == inspection_id - ).order_by(models.Question.section, models.Question.order).all() - - # Crear PDF en memoria - buffer = BytesIO() - doc = SimpleDocTemplate(buffer, pagesize=A4, rightMargin=30, leftMargin=30, topMargin=30, bottomMargin=30) - elements = [] - styles = getSampleStyleSheet() - title_style = styles['Title'] - normal_style = styles['Normal'] - header_style = ParagraphStyle('Header', parent=styles['Heading2'], alignment=TA_CENTER, spaceAfter=12) - # Portada - elements.append(Paragraph(f"Informe de Inspección #{inspection.id}", title_style)) - elements.append(Spacer(1, 12)) - elements.append(Paragraph(f"Vehículo: {inspection.vehicle_brand or ''} {inspection.vehicle_model or ''} - Placa: {inspection.vehicle_plate}", normal_style)) - elements.append(Paragraph(f"Cliente: {inspection.client_name or ''}", normal_style)) - elements.append(Paragraph(f"Mecánico: {mechanic.full_name if mechanic else ''}", normal_style)) - elements.append(Paragraph(f"Checklist: {checklist.name if checklist else ''}", normal_style)) - elements.append(Paragraph(f"Fecha: {inspection.started_at.strftime('%d/%m/%Y %H:%M') if inspection.started_at else ''}", normal_style)) - elements.append(Spacer(1, 18)) - # Tabla de respuestas - table_data = [["Sección", "Pregunta", "Respuesta", "Estado", "Comentario", "Miniaturas"]] - for ans in answers: - question = ans.question - media_imgs = [] - for media in ans.media_files: - if media.file_type == "image": - try: - # Descargar imagen y agregar miniatura - import requests - img_resp = requests.get(media.file_path) - if img_resp.status_code == 200: - img_bytes = BytesIO(img_resp.content) - rl_img = RLImage(img_bytes, width=0.7*inch, height=0.7*inch) - media_imgs.append(rl_img) - except Exception as e: - print(f"Error cargando imagen {media.file_path}: {e}") - row = [ - question.section or "", - question.text, - ans.answer_value, - ans.status, - ans.comment or "", - media_imgs if media_imgs else "" - ] - table_data.append(row) - # Construir tabla con miniaturas - table = Table(table_data, colWidths=[1.2*inch, 2.5*inch, 1*inch, 0.8*inch, 2*inch, 1.5*inch]) - table.setStyle(TableStyle([ - ('BACKGROUND', (0,0), (-1,0), colors.lightgrey), - ('TEXTCOLOR', (0,0), (-1,0), colors.black), - ('ALIGN', (0,0), (-1,-1), 'LEFT'), - ('VALIGN', (0,0), (-1,-1), 'TOP'), - ('FONTNAME', (0,0), (-1,0), 'Helvetica-Bold'), - ('FONTSIZE', (0,0), (-1,0), 10), - ('BOTTOMPADDING', (0,0), (-1,0), 8), - ('GRID', (0,0), (-1,-1), 0.5, colors.grey), - ])) - elements.append(table) - elements.append(Spacer(1, 18)) - # Pie de página - elements.append(Paragraph(f"Generado por Checklist Inteligente - {datetime.now().strftime('%d/%m/%Y %H:%M')}", header_style)) - # Construir PDF - try: - doc.build(elements) - except Exception as e: - print(f"Error al generar PDF: {e}") - buffer.seek(0) - pdf_size = len(buffer.getvalue()) - print(f"PDF generado, tamaño: {pdf_size} bytes") - # Guardar localmente para depuración - try: - with open(f"/tmp/test_inspeccion_{inspection_id}.pdf", "wb") as f: - f.write(buffer.getvalue()) - except Exception as e: - print(f"No se pudo guardar PDF local: {e}") - now = datetime.now() - folder = f"{now.year}/{now.month:02d}" - filename = f"inspeccion_{inspection_id}_{inspection.vehicle_plate or 'sin-patente'}.pdf" - s3_key = f"{folder}/{filename}" - # Subir PDF a S3/MinIO - buffer.seek(0) # Asegura que el puntero esté al inicio - s3_client.upload_fileobj(buffer, S3_PDF_BUCKET, s3_key, ExtraArgs={"ContentType": "application/pdf"}) - pdf_url = f"{S3_ENDPOINT}/{S3_PDF_BUCKET}/{s3_key}" - inspection.pdf_url = pdf_url - db.commit() - # Descargar el PDF directamente - buffer.seek(0) - return StreamingResponse(buffer, media_type="application/pdf", headers={ - "Content-Disposition": f"attachment; filename={filename}" - }) + # Si existe pdf_url, descargar desde MinIO y devolverlo + if inspection.pdf_url: + try: + pdf_resp = requests.get(inspection.pdf_url, stream=True) + if pdf_resp.status_code == 200: + filename = inspection.pdf_url.split("/")[-1] + return StreamingResponse(pdf_resp.raw, media_type="application/pdf", headers={ + "Content-Disposition": f"attachment; filename={filename}" + }) + else: + raise HTTPException(status_code=404, detail="No se pudo descargar el PDF desde MinIO") + except Exception as e: + raise HTTPException(status_code=500, detail=f"Error al descargar PDF: {e}") + else: + raise HTTPException(status_code=404, detail="La inspección no tiene PDF generado") # ============= HEALTH CHECK ============= diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index f3b58d4..e05ac83 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1865,17 +1865,39 @@ function InspectionDetailModal({ inspection, user, onClose, onUpdate }) { const response = await fetch(`${API_URL}/api/inspections/${inspection.id}/pdf`, { headers: { 'Authorization': `Bearer ${token}` } }) - if (response.ok) { - const blob = await response.blob() - const url = window.URL.createObjectURL(blob) - const a = document.createElement('a') - a.href = url - a.download = `inspeccion_${inspection.id}_${inspection.vehicle_plate || 'sin-patente'}.pdf` - document.body.appendChild(a) - a.click() - document.body.removeChild(a) - window.URL.revokeObjectURL(url) + const contentType = response.headers.get('content-type') + if (contentType && contentType.includes('application/pdf')) { + const blob = await response.blob() + const url = window.URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = `inspeccion_${inspection.id}_${inspection.vehicle_plate || 'sin-patente'}.pdf` + document.body.appendChild(a) + a.click() + document.body.removeChild(a) + window.URL.revokeObjectURL(url) + } else { + 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') }