diff --git a/backend/app/main.py b/backend/app/main.py index aa5e19a..d60b274 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -204,7 +204,7 @@ def send_completed_inspection_to_n8n(inspection, db): # No lanzamos excepción para no interrumpir el flujo normal -BACKEND_VERSION = "1.0.74" +BACKEND_VERSION = "1.0.75" app = FastAPI(title="Checklist Inteligente API", version=BACKEND_VERSION) # S3/MinIO configuration @@ -1374,58 +1374,98 @@ def generate_inspection_pdf(inspection_id: int, db: Session) -> str: checklist = db.query(models.Checklist).filter(models.Checklist.id == inspection.checklist_id).first() # ===== PORTADA ===== - elements.append(Spacer(1, 10*mm)) + elements.append(Spacer(1, 15*mm)) + + # Título con diseño moderno elements.append(Paragraph("📋 INFORME DE INSPECCIÓN VEHICULAR", title_style)) elements.append(Paragraph(f"N° {inspection.id}", subtitle_style)) - elements.append(Spacer(1, 5*mm)) + elements.append(Spacer(1, 10*mm)) - # Cuadro de información del vehículo - vehicle_data = [ - [Paragraph("🚗 INFORMACIÓN DEL VEHÍCULO", info_style)], - [Table([ - [Paragraph("Placa:", small_style), Paragraph(f"{inspection.vehicle_plate}", info_style)], - [Paragraph("Marca:", small_style), Paragraph(f"{inspection.vehicle_brand or 'N/A'}", info_style)], - [Paragraph("Modelo:", small_style), Paragraph(f"{inspection.vehicle_model or 'N/A'}", info_style)], - [Paragraph("Kilometraje:", small_style), Paragraph(f"{inspection.vehicle_km or 'N/A'} km", info_style)] - ], colWidths=[30*mm, 50*mm])] - ] + # Estilo para etiquetas de información + label_style = ParagraphStyle( + 'LabelStyle', + parent=styles['Normal'], + fontSize=9, + textColor=colors.HexColor('#64748b'), + spaceAfter=2 + ) - vehicle_table = Table(vehicle_data, colWidths=[85*mm]) - vehicle_table.setStyle(TableStyle([ - ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#dbeafe')), - ('TEXTCOLOR', (0, 0), (-1, 0), colors.HexColor('#1e40af')), + value_style = ParagraphStyle( + 'ValueStyle', + parent=styles['Normal'], + fontSize=11, + textColor=colors.HexColor('#1e293b'), + fontName='Helvetica-Bold' + ) + + # Cuadro de información del vehículo con diseño moderno + vehicle_header = Table( + [[Paragraph("🚗 INFORMACIÓN DEL VEHÍCULO", ParagraphStyle('veh_header', parent=info_style, fontSize=12, textColor=colors.white))]], + colWidths=[85*mm] + ) + vehicle_header.setStyle(TableStyle([ + ('BACKGROUND', (0, 0), (-1, -1), colors.HexColor('#2563eb')), + ('PADDING', (0, 0), (-1, -1), 10), ('ALIGN', (0, 0), (-1, -1), 'LEFT'), - ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'), - ('FONTSIZE', (0, 0), (-1, 0), 11), - ('PADDING', (0, 0), (-1, -1), 8), - ('BACKGROUND', (0, 1), (-1, -1), colors.white), - ('BOX', (0, 0), (-1, -1), 1, colors.HexColor('#93c5fd')), - ('VALIGN', (0, 0), (-1, -1), 'TOP'), + ('ROUNDEDCORNERS', [6, 6, 0, 0]), ])) - # Cuadro de información del cliente e inspección - inspection_data = [ - [Paragraph("👤 INFORMACIÓN DEL CLIENTE", info_style)], - [Table([ - [Paragraph("Nº Pedido:", small_style), Paragraph(f"{inspection.order_number or 'N/A'}", info_style)], - [Paragraph("OR N°:", small_style), Paragraph(f"{inspection.or_number or 'N/A'}", info_style)], - [Paragraph("Mecánico:", small_style), Paragraph(f"{mechanic.full_name if mechanic else 'N/A'}", info_style)], - [Paragraph("Cód. Operario:", small_style), Paragraph(f"{inspection.mechanic_employee_code or 'N/A'}", info_style)], - [Paragraph("Fecha:", small_style), Paragraph(f"{inspection.started_at.strftime('%d/%m/%Y %H:%M') if inspection.started_at else 'N/A'}", info_style)] - ], colWidths=[30*mm, 50*mm])] - ] - - inspection_info_table = Table(inspection_data, colWidths=[85*mm]) - inspection_info_table.setStyle(TableStyle([ - ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#dcfce7')), - ('TEXTCOLOR', (0, 0), (-1, 0), colors.HexColor('#15803d')), - ('ALIGN', (0, 0), (-1, -1), 'LEFT'), - ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'), - ('FONTSIZE', (0, 0), (-1, 0), 11), - ('PADDING', (0, 0), (-1, -1), 8), - ('BACKGROUND', (0, 1), (-1, -1), colors.white), - ('BOX', (0, 0), (-1, -1), 1, colors.HexColor('#86efac')), + vehicle_content = Table([ + [Paragraph("Placa", label_style), Paragraph(f"{inspection.vehicle_plate}", value_style)], + [Paragraph("Marca", label_style), Paragraph(f"{inspection.vehicle_brand or 'N/A'}", value_style)], + [Paragraph("Modelo", label_style), Paragraph(f"{inspection.vehicle_model or 'N/A'}", value_style)], + [Paragraph("Kilometraje", label_style), Paragraph(f"{inspection.vehicle_km or 'N/A'} km", value_style)] + ], colWidths=[25*mm, 60*mm]) + vehicle_content.setStyle(TableStyle([ + ('PADDING', (0, 0), (-1, -1), 10), + ('BACKGROUND', (0, 0), (-1, -1), colors.white), ('VALIGN', (0, 0), (-1, -1), 'TOP'), + ('LINEBELOW', (0, 0), (-1, -2), 0.5, colors.HexColor('#e2e8f0')), + ])) + + vehicle_table = Table( + [[vehicle_header], [vehicle_content]], + colWidths=[85*mm] + ) + vehicle_table.setStyle(TableStyle([ + ('BOX', (0, 0), (-1, -1), 1.5, colors.HexColor('#2563eb')), + ('VALIGN', (0, 0), (-1, -1), 'TOP'), + ('ROUNDEDCORNERS', [6, 6, 6, 6]), + ])) + + # Cuadro de información del cliente e inspección (sin nombre de mecánico por privacidad) + client_header = Table( + [[Paragraph("📄 INFORMACIÓN DE LA INSPECCIÓN", ParagraphStyle('client_header', parent=info_style, fontSize=12, textColor=colors.white))]], + colWidths=[85*mm] + ) + client_header.setStyle(TableStyle([ + ('BACKGROUND', (0, 0), (-1, -1), colors.HexColor('#16a34a')), + ('PADDING', (0, 0), (-1, -1), 10), + ('ALIGN', (0, 0), (-1, -1), 'LEFT'), + ('ROUNDEDCORNERS', [6, 6, 0, 0]), + ])) + + client_content = Table([ + [Paragraph("Nº Pedido", label_style), Paragraph(f"{inspection.order_number or 'N/A'}", value_style)], + [Paragraph("OR N°", label_style), Paragraph(f"{inspection.or_number or 'N/A'}", value_style)], + [Paragraph("Cód. Operario", label_style), Paragraph(f"{inspection.mechanic_employee_code or 'N/A'}", value_style)], + [Paragraph("Fecha", label_style), Paragraph(f"{inspection.started_at.strftime('%d/%m/%Y %H:%M') if inspection.started_at else 'N/A'}", value_style)] + ], colWidths=[25*mm, 60*mm]) + client_content.setStyle(TableStyle([ + ('PADDING', (0, 0), (-1, -1), 10), + ('BACKGROUND', (0, 0), (-1, -1), colors.white), + ('VALIGN', (0, 0), (-1, -1), 'TOP'), + ('LINEBELOW', (0, 0), (-1, -2), 0.5, colors.HexColor('#e2e8f0')), + ])) + + inspection_info_table = Table( + [[client_header], [client_content]], + colWidths=[85*mm] + ) + inspection_info_table.setStyle(TableStyle([ + ('BOX', (0, 0), (-1, -1), 1.5, colors.HexColor('#16a34a')), + ('VALIGN', (0, 0), (-1, -1), 'TOP'), + ('ROUNDEDCORNERS', [6, 6, 6, 6]), ])) # Tabla con ambos cuadros lado a lado @@ -1437,28 +1477,48 @@ def generate_inspection_pdf(inspection_id: int, db: Session) -> str: elements.append(info_table) elements.append(Spacer(1, 8*mm)) - # Resumen de puntuación + # Resumen de puntuación con diseño mejorado percentage = inspection.percentage score_color = colors.HexColor('#22c55e') if percentage >= 80 else colors.HexColor('#eab308') if percentage >= 60 else colors.HexColor('#ef4444') + score_label = "EXCELENTE" if percentage >= 80 else "ACEPTABLE" if percentage >= 60 else "DEFICIENTE" - score_data = [ + # Título de resumen + score_title = Table( + [[Paragraph("📊 RESUMEN DE EVALUACIÓN", ParagraphStyle('score_title', parent=info_style, fontSize=14, textColor=colors.HexColor('#1e293b'), alignment=TA_CENTER))]], + colWidths=[180*mm] + ) + score_title.setStyle(TableStyle([ + ('PADDING', (0, 0), (-1, -1), 8), + ('ALIGN', (0, 0), (-1, -1), 'CENTER'), + ])) + elements.append(score_title) + elements.append(Spacer(1, 3*mm)) + + # Cuadro de métricas con diseño moderno + metric_label = ParagraphStyle('metric_label', parent=small_style, fontSize=9, textColor=colors.HexColor('#64748b'), alignment=TA_CENTER) + metric_value = ParagraphStyle('metric_value', parent=info_style, fontSize=16, fontName='Helvetica-Bold', alignment=TA_CENTER) + + metrics_data = [ + [Paragraph("Puntuación", metric_label), Paragraph("Porcentaje", metric_label), Paragraph("Estado", metric_label), Paragraph("Ítems Críticos", metric_label)], [ - Paragraph("Puntuación Total", info_style), - Paragraph(f"{inspection.score} / {inspection.max_score}", info_style), - Paragraph(f"{percentage:.1f}%", info_style), - Paragraph(f"⚠️ {inspection.flagged_items_count} Críticos", info_style) + Paragraph(f"{inspection.score} / {inspection.max_score}", metric_value), + Paragraph(f"{percentage:.1f}%", metric_value), + Paragraph(f"{score_label}", ParagraphStyle('status_value', parent=metric_value, textColor=score_color)), + Paragraph(f"{inspection.flagged_items_count}", metric_value) ] ] - score_table = Table(score_data, colWidths=[45*mm, 45*mm, 45*mm, 45*mm]) + score_table = Table(metrics_data, colWidths=[45*mm, 45*mm, 45*mm, 45*mm]) score_table.setStyle(TableStyle([ - ('BACKGROUND', (0, 0), (-1, -1), score_color), - ('TEXTCOLOR', (0, 0), (-1, -1), colors.white), + ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#f8fafc')), + ('BACKGROUND', (0, 1), (-1, -1), colors.white), ('ALIGN', (0, 0), (-1, -1), 'CENTER'), - ('FONTNAME', (0, 0), (-1, -1), 'Helvetica-Bold'), - ('FONTSIZE', (0, 0), (-1, -1), 12), - ('PADDING', (0, 0), (-1, -1), 10), - ('BOX', (0, 0), (-1, -1), 2, colors.white), + ('VALIGN', (0, 0), (-1, -1), 'MIDDLE'), + ('PADDING', (0, 0), (-1, -1), 12), + ('BOX', (0, 0), (-1, -1), 2, score_color), + ('LINEABOVE', (0, 1), (-1, 1), 1.5, score_color), + ('GRID', (0, 0), (-1, -1), 0.5, colors.HexColor('#e2e8f0')), + ('ROUNDEDCORNERS', [8, 8, 8, 8]), ])) elements.append(score_table) elements.append(PageBreak())