diff --git a/backend/app/main.py b/backend/app/main.py index 52d8b77..b15467b 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -83,6 +83,124 @@ def send_answer_notification(answer, question, mechanic, db): print(f"❌ Error enviando notificación: {e}") # No lanzamos excepción para no interrumpir el flujo normal + +def send_completed_inspection_to_n8n(inspection, db): + """Envía la inspección completa con todas las respuestas e imágenes a n8n""" + try: + if not app_config.settings.NOTIFICACION_ENDPOINT: + print("No hay endpoint de notificación configurado") + return + + print(f"\n🚀 Enviando inspección #{inspection.id} a n8n...") + + # Obtener datos del mecánico + mechanic = db.query(models.User).filter(models.User.id == inspection.mechanic_id).first() + + # Obtener checklist + checklist = db.query(models.Checklist).filter(models.Checklist.id == inspection.checklist_id).first() + + # Obtener todas las respuestas con sus imágenes + answers = db.query(models.Answer).options( + joinedload(models.Answer.media_files), + joinedload(models.Answer.question) + ).filter(models.Answer.inspection_id == inspection.id).all() + + # Preparar respuestas con imágenes + respuestas_data = [] + for answer in answers: + # Obtener URLs de imágenes + imagenes = [] + for media in answer.media_files: + if media.file_type == "image": + imagenes.append({ + "id": media.id, + "url": media.file_path, + "filename": media.filename + }) + + respuestas_data.append({ + "id": answer.id, + "pregunta": { + "id": answer.question.id, + "texto": answer.question.text, + "seccion": answer.question.section, + "orden": answer.question.order + }, + "respuesta": answer.answer_value, + "estado": answer.status, + "comentario": answer.comment, + "observaciones": answer.observations, + "puntos_obtenidos": answer.points_earned, + "es_critico": answer.is_flagged, + "imagenes": imagenes, + "ai_analysis": answer.ai_analysis + }) + + # Preparar datos completos de la inspección + inspeccion_data = { + "tipo": "inspeccion_completada", + "inspeccion": { + "id": inspection.id, + "estado": inspection.status, + "or_number": inspection.or_number, + "work_order_number": inspection.work_order_number, + "vehiculo": { + "placa": inspection.vehicle_plate, + "marca": inspection.vehicle_brand, + "modelo": inspection.vehicle_model, + "kilometraje": inspection.vehicle_km + }, + "cliente": inspection.client_name, + "mecanico": { + "id": mechanic.id if mechanic else None, + "nombre": mechanic.full_name if mechanic else None, + "email": mechanic.email if mechanic else None, + "codigo_operario": inspection.mechanic_employee_code + }, + "checklist": { + "id": checklist.id if checklist else None, + "nombre": checklist.name if checklist else None + }, + "puntuacion": { + "obtenida": inspection.score, + "maxima": inspection.max_score, + "porcentaje": round(inspection.percentage, 2), + "items_criticos": inspection.flagged_items_count + }, + "fechas": { + "inicio": inspection.started_at.isoformat() if inspection.started_at else None, + "completado": inspection.completed_at.isoformat() if inspection.completed_at else None + }, + "pdf_url": inspection.pdf_url, + "firma": inspection.signature_data + }, + "respuestas": respuestas_data, + "timestamp": datetime.utcnow().isoformat() + } + + # Enviar al webhook de n8n + print(f"📤 Enviando {len(respuestas_data)} respuestas con imágenes a n8n...") + response = requests.post( + app_config.settings.NOTIFICACION_ENDPOINT, + json=inspeccion_data, + timeout=30 # Timeout más largo para inspecciones completas + ) + + if response.status_code == 200: + print(f"✅ Inspección #{inspection.id} enviada exitosamente a n8n") + print(f" - {len(respuestas_data)} respuestas") + print(f" - {sum(len(r['imagenes']) for r in respuestas_data)} imágenes") + else: + print(f"⚠️ Error al enviar inspección a n8n: {response.status_code}") + print(f" Response: {response.text[:200]}") + + except Exception as e: + print(f"❌ Error enviando inspección a n8n: {e}") + import traceback + traceback.print_exc() + # No lanzamos excepción para no interrumpir el flujo normal + + BACKEND_VERSION = "1.0.25" app = FastAPI(title="Checklist Inteligente API", version=BACKEND_VERSION) @@ -1028,6 +1146,10 @@ def complete_inspection( inspection.pdf_url = pdf_url db.commit() db.refresh(inspection) + + # Enviar inspección completa a n8n con todas las respuestas e imágenes + send_completed_inspection_to_n8n(inspection, db) + return inspection diff --git a/docs/webhook-n8n.md b/docs/webhook-n8n.md new file mode 100644 index 0000000..b57df76 --- /dev/null +++ b/docs/webhook-n8n.md @@ -0,0 +1,196 @@ +# Documentación de Webhook - n8n + +## Endpoint +El endpoint configurado en `.env`: +``` +NOTIFICACION_ENDPOINT=https://n8nw.comercialarmin.com.py/webhook/53284540-edc4-418f-b1bf-a70a805f8212 +``` + +## Evento: Inspección Completada + +### Cuándo se envía +Cuando se completa una inspección (endpoint: `POST /api/inspections/{id}/complete`) + +### Estructura del JSON + +```json +{ + "tipo": "inspeccion_completada", + "inspeccion": { + "id": 123, + "estado": "completed", + "or_number": "OR-001", + "work_order_number": "WO-123", + "vehiculo": { + "placa": "ABC-123", + "marca": "Toyota", + "modelo": "Corolla 2020", + "kilometraje": 50000 + }, + "cliente": "Juan Pérez", + "mecanico": { + "id": 5, + "nombre": "Carlos Méndez", + "email": "carlos@example.com", + "codigo_operario": "OPR-001" + }, + "checklist": { + "id": 1, + "nombre": "Inspección Vehicular Completa" + }, + "puntuacion": { + "obtenida": 85, + "maxima": 100, + "porcentaje": 85.0, + "items_criticos": 2 + }, + "fechas": { + "inicio": "2025-11-26T10:30:00", + "completado": "2025-11-26T11:45:00" + }, + "pdf_url": "https://minioapi.ayutec.es/pdfs/2025/11/inspeccion_123_ABC-123.pdf", + "firma": "data:image/png;base64,..." + }, + "respuestas": [ + { + "id": 1, + "pregunta": { + "id": 10, + "texto": "¿Estado de los neumáticos?", + "seccion": "Neumáticos", + "orden": 1 + }, + "respuesta": "ok", + "estado": "ok", + "comentario": "Neumáticos en buen estado", + "observaciones": "Presión correcta en las 4 ruedas", + "puntos_obtenidos": 1, + "es_critico": false, + "imagenes": [ + { + "id": 100, + "url": "https://minioapi.ayutec.es/images/2025/11/foto1.jpg", + "filename": "neumatico_delantero.jpg" + }, + { + "id": 101, + "url": "https://minioapi.ayutec.es/images/2025/11/foto2.jpg", + "filename": "neumatico_trasero.jpg" + } + ], + "ai_analysis": { + "status": "ok", + "observations": "Los neumáticos presentan un desgaste uniforme...", + "recommendation": "Continuar con el mantenimiento preventivo", + "confidence": 0.95 + } + }, + { + "id": 2, + "pregunta": { + "id": 11, + "texto": "¿Luces delanteras funcionan?", + "seccion": "Iluminación", + "orden": 2 + }, + "respuesta": "warning", + "estado": "warning", + "comentario": "Faro izquierdo opaco", + "observaciones": "Requiere restauración de faro", + "puntos_obtenidos": 0.5, + "es_critico": true, + "imagenes": [ + { + "id": 102, + "url": "https://minioapi.ayutec.es/images/2025/11/foto3.jpg", + "filename": "faro_izquierdo.jpg" + } + ], + "ai_analysis": { + "status": "minor", + "observations": "Se detecta opacidad en el faro izquierdo...", + "recommendation": "Pulir o restaurar el lente del faro", + "confidence": 0.9 + } + } + ], + "timestamp": "2025-11-26T11:45:30.123456" +} +``` + +## Campos Importantes + +### Imágenes +- Cada respuesta incluye un array `imagenes` con: + - `id`: ID del archivo en la base de datos + - `url`: **URL directa** de la imagen en MinIO (lista para descargar/mostrar) + - `filename`: Nombre original del archivo + +### AI Analysis +- Si la pregunta fue analizada por IA, incluye: + - `status`: ok/minor/critical + - `observations`: Observaciones del análisis + - `recommendation`: Recomendaciones + - `confidence`: Nivel de confianza (0-1) + +### Código de Operario +- Se incluye en `inspeccion.mecanico.codigo_operario` +- Se copia automáticamente del perfil del mecánico al crear la inspección + +### PDF +- URL del PDF generado en `inspeccion.pdf_url` +- Incluye miniaturas de todas las imágenes + +## Uso en n8n + +### 1. Webhook Trigger +Configura un nodo Webhook con la URL del archivo `.env` + +### 2. Filtrar por tipo +```javascript +// Verificar si es una inspección completada +{{ $json.tipo === "inspeccion_completada" }} +``` + +### 3. Acceder a las imágenes +```javascript +// Obtener todas las URLs de imágenes +{{ $json.respuestas.map(r => r.imagenes.map(i => i.url)).flat() }} + +// Primera imagen de cada respuesta +{{ $json.respuestas.map(r => r.imagenes[0]?.url) }} + +// Imágenes de respuestas críticas +{{ $json.respuestas.filter(r => r.es_critico).map(r => r.imagenes).flat() }} +``` + +### 4. Descargar imágenes +Las URLs son públicas y directas, se pueden: +- Descargar con HTTP Request +- Enviar por email como adjuntos +- Procesar con Computer Vision +- Subir a otro servicio (Google Drive, Dropbox, etc.) + +### 5. Ejemplo: Enviar por email +```javascript +// En un nodo Email +To: {{ $json.inspeccion.cliente_email }} +Subject: Inspección Completada - {{ $json.inspeccion.vehiculo.placa }} +Attachments: {{ $json.inspeccion.pdf_url }} +``` + +## Logs +El backend imprime logs detallados: +``` +🚀 Enviando inspección #123 a n8n... +📤 Enviando 15 respuestas con imágenes a n8n... +✅ Inspección #123 enviada exitosamente a n8n + - 15 respuestas + - 23 imágenes +``` + +## Seguridad +- El webhook es HTTPS +- Las URLs de imágenes son públicas en MinIO +- No se envían passwords ni tokens +- Se incluyen solo datos relevantes de la inspección