{ "name": "Generar reporte de rutas PDF", "nodes": [ { "parameters": { "httpMethod": "POST", "path": "generar-rutas-pdf", "responseMode": "responseNode", "options": {} }, "type": "n8n-nodes-base.webhook", "typeVersion": 2.1, "position": [-600, 260], "id": "e91ef4ce-4c71-4a7b-91d7-1a4f1c2db015", "name": "Webhook: Generate PDF" }, { "parameters": { "jsCode": "const payload = $json.body && typeof $json.body === 'object' ? $json.body : $json;\nconst errors = [];\nif (!payload?.project?.id || !payload?.project?.name) errors.push('Se requiere la información del proyecto.');\nif (!Array.isArray(payload?.assignedRoutes)) errors.push('assignedRoutes debe ser una lista.');\nif (!Array.isArray(payload?.unassignedPdvs)) errors.push('unassignedPdvs debe ser una lista.');\nif ((payload?.assignedRoutes?.length ?? 0) + (payload?.unassignedPdvs?.length ?? 0) > 5000) errors.push('El reporte excede el máximo de 5,000 registros.');\nif (errors.length) throw new Error(errors.join(' '));\nreturn [{ json: { payload } }];" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [-360, 260], "id": "abbd2d6e-5c04-4bbb-ae1b-e4f0d2e4574e", "name": "Validate Report Payload", "onError": "continueErrorOutput" }, { "parameters": { "jsCode": "const payload = $json.payload;\nconst escape = value => String(value ?? '').replace(/[&<>\"']/g, char => ({ '&': '&', '<': '<', '>': '>', '\"': '"', \"'\": ''' }[char]));\nconst filters = Object.entries(payload.filters ?? {}).filter(([, value]) => value).map(([key, value]) => `${escape(key)}: ${escape(value)}`).join('');\nconst routeRows = payload.assignedRoutes.map(route => `${escape(route.usuario)}${escape(route.dia)}${escape(route.orden)}${escape(route.pdv)}${escape(route.origen)}${escape(route.distanciaKm)} km${escape(route.horasTrabajo)} h`).join('') || 'Sin rutas asignadas para los filtros seleccionados.';\nconst unassignedRows = payload.unassignedPdvs.map(route => `${escape(route.pdv_id)}${escape(route.pdv_nombre)}${escape(route.minimo_horas_semana)} h${escape(route.motivo)}${escape(route.recomendacion)}`).join('') || 'No hay PDVs sin asignar para los filtros seleccionados.';\nconst summary = payload.summary ?? {};\nconst html = `

Reporte de rutas

${escape(payload.project.name)}
${escape(payload.project.country)}${payload.project.description ? `
${escape(payload.project.description)}` : ''}
Generado: ${escape(payload.generatedAt)}

${filters ? `
Filtros aplicados:
${filters}
` : '

Filtros aplicados: Ninguno

'}
${escape(summary.totalPdvs)}PDVs totales
${escape(summary.assignedPdvs)}PDVs asignados
${escape(summary.unassignedPdvs)}PDVs sin asignar
${escape(summary.coveragePercent)}%Cobertura
${escape(summary.totalDistanceKm)} kmDistancia
${escape(summary.totalWorkHours)} hTrabajo

Rutas asignadas

${routeRows}
UsuarioDíaOrdenPDVOrigenDistanciaTrabajo

PDVs sin asignar

${unassignedRows}
PDV IDPDVHoras/semanaMotivoRecomendación

Route Planner · GomezLee Marketing

`;\nreturn [{ json: { fileName: `rutas_${String(payload.project.name).replace(/[^a-z0-9]+/gi, '_').toLowerCase()}.pdf` }, binary: { data: { data: Buffer.from(html).toString('base64'), mimeType: 'text/html', fileName: 'index.html' } } }];" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [-100, 260], "id": "9258c394-683f-4569-8ced-af2337ec8dba", "name": "Build Report HTML", "onError": "continueErrorOutput", "notes": "Genera tablas deterministas; no usa IA para alterar cifras, rutas ni recomendaciones." }, { "parameters": { "method": "POST", "url": "http://host.docker.internal:3000/forms/chromium/convert/html", "authentication": "none", "sendBody": true, "contentType": "multipart-form-data", "bodyParameters": { "parameters": [ { "parameterType": "formBinaryData", "name": "files", "inputDataFieldName": "data" } ] }, "options": { "response": { "response": { "responseFormat": "file", "outputPropertyName": "data" } }, "timeout": 120000 } }, "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [180, 260], "id": "73e2759c-d9f9-49dc-925f-3ad1ee7b3897", "name": "Convert HTML to PDF", "retryOnFail": true, "maxTries": 3, "waitBetweenTries": 3000, "onError": "continueErrorOutput", "notes": "Gotenberg debe estar disponible en host.docker.internal:3000 desde el contenedor n8n." }, { "parameters": { "respondWith": "binary", "responseBinaryPropertyName": "data", "options": { "responseHeaders": { "entries": [ { "name": "Content-Disposition", "value": "attachment; filename=\"reporte_rutas.pdf\"" } ] } } }, "type": "n8n-nodes-base.respondToWebhook", "typeVersion": 1.5, "position": [460, 200], "id": "93928c3e-1e9c-4f77-a644-c156fae907a5", "name": "Respond PDF" }, { "parameters": { "respondWith": "json", "responseBody": "={{ { error: 'validation_error', message: $json.error?.message || $json.message || 'El reporte no contiene datos válidos.' } }}", "options": { "responseCode": 400 } }, "type": "n8n-nodes-base.respondToWebhook", "typeVersion": 1.5, "position": [-100, 460], "id": "c8b69f14-45d0-4f55-9f7c-2e3fb3425f14", "name": "Respond Validation Error" }, { "parameters": { "respondWith": "json", "responseBody": "={{ { error: 'pdf_generation_error', message: 'No se pudo generar el PDF de rutas.', detail: $json.error?.message || $json.message || 'Gotenberg no está disponible.' } }}", "options": { "responseCode": 502 } }, "type": "n8n-nodes-base.respondToWebhook", "typeVersion": 1.5, "position": [460, 430], "id": "c8d6e363-80b6-46ca-a8c6-32ce393207cc", "name": "Respond PDF Error" } ], "connections": { "Webhook: Generate PDF": { "main": [[{ "node": "Validate Report Payload", "type": "main", "index": 0 }]] }, "Validate Report Payload": { "main": [[{ "node": "Build Report HTML", "type": "main", "index": 0 }], [{ "node": "Respond Validation Error", "type": "main", "index": 0 }]] }, "Build Report HTML": { "main": [[{ "node": "Convert HTML to PDF", "type": "main", "index": 0 }], [{ "node": "Respond Validation Error", "type": "main", "index": 0 }]] }, "Convert HTML to PDF": { "main": [[{ "node": "Respond PDF", "type": "main", "index": 0 }], [{ "node": "Respond PDF Error", "type": "main", "index": 0 }]] } }, "active": false, "settings": { "executionOrder": "v1" }, "meta": { "templateCredsSetupCompleted": true }, "tags": [] }