223 lines
9.8 KiB
JSON
223 lines
9.8 KiB
JSON
{
|
|
"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]) => `<span>${escape(key)}: ${escape(value)}</span>`).join('');\nconst routeRows = payload.assignedRoutes.map(route => `<tr><td>${escape(route.usuario)}</td><td>${escape(route.dia)}</td><td>${escape(route.orden)}</td><td>${escape(route.pdv)}</td><td>${escape(route.origen)}</td><td>${escape(route.distanciaKm)} km</td><td>${escape(route.horasTrabajo)} h</td><td>${route.costoCombustible != null ? `${escape(route.monedaCombustible)} ${escape(route.costoCombustible)}` : '\\u2014'}</td></tr>`).join('') || '<tr><td colspan=\"8\">Sin rutas asignadas para los filtros seleccionados.</td></tr>';\nconst unassignedRows = payload.unassignedPdvs.map(route => `<tr><td>${escape(route.pdv_id)}</td><td>${escape(route.pdv_nombre)}</td><td>${escape(route.minimo_horas_semana)} h</td><td>${escape(route.motivo)}</td><td>${escape(route.recomendacion)}</td></tr>`).join('') || '<tr><td colspan=\"5\">No hay PDVs sin asignar para los filtros seleccionados.</td></tr>';\nconst summary = payload.summary ?? {};const fuel = payload.fuel ?? {};\nconst html = `<!doctype html><html><head><meta charset=\"utf-8\"><style>body{font-family:Arial,sans-serif;color:#243746;margin:28px;font-size:11px}h1{color:#4F758B;margin:0}h2{color:#4F758B;border-bottom:2px solid #6CC24A;padding-bottom:6px;margin-top:28px}p{line-height:1.45}.meta{color:#61798a}.filters span{display:inline-block;background:#eef6e8;border:1px solid #a4d65e;border-radius:3px;margin:3px 5px 3px 0;padding:4px 6px}.summary{display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin:15px 0}.metric{border:1px solid #d0d0d0;border-left:3px solid #6CC24A;padding:8px}.metric b{display:block;color:#4F758B;font-size:16px}table{width:100%;border-collapse:collapse;margin-top:10px}th{background:#4F758B;color:#fff;text-align:left;padding:7px}td{border-bottom:1px solid #e5e5e5;padding:7px;vertical-align:top}.footer{margin-top:24px;color:#6b8fa3;font-size:9px}</style></head><body><h1>Reporte de rutas</h1><p class=\"meta\"><b>${escape(payload.project.name)}</b><br>${escape(payload.project.country)}${payload.project.description ? `<br>${escape(payload.project.description)}` : ''}<br>Generado: ${escape(payload.generatedAt)}</p>${filters ? `<div class=\"filters\"><b>Filtros aplicados:</b><br>${filters}</div>` : '<p><b>Filtros aplicados:</b> Ninguno</p>'}<div class=\"summary\"><div class=\"metric\"><b>${escape(summary.totalPdvs)}</b>PDVs totales</div><div class=\"metric\"><b>${escape(summary.assignedPdvs)}</b>PDVs asignados</div><div class=\"metric\"><b>${escape(summary.unassignedPdvs)}</b>PDVs sin asignar</div><div class=\"metric\"><b>${escape(summary.coveragePercent)}%</b>Cobertura</div><div class=\"metric\"><b>${escape(summary.totalDistanceKm)} km</b>Distancia</div><div class=\"metric\"><b>${escape(summary.totalWorkHours)} h</b>Trabajo</div><div class=\"metric\"><b>${fuel.configured ? `${escape(summary.fuelLiters)} L` : '\\u2014'}</b>Combustible</div><div class=\"metric\"><b>${fuel.configured ? `${escape(summary.fuelCurrency)} ${escape(summary.fuelCost)}` : '\\u2014'}</b>Costo combustible</div></div><h2>Rutas asignadas</h2><table><thead><tr><th>Usuario</th><th>Día</th><th>Orden</th><th>PDV</th><th>Origen</th><th>Distancia</th><th>Trabajo</th><th>Costo comb.</th></tr></thead><tbody>${routeRows}</tbody></table><h2>PDVs sin asignar</h2><table><thead><tr><th>PDV ID</th><th>PDV</th><th>Horas/semana</th><th>Motivo</th><th>Recomendación</th></tr></thead><tbody>${unassignedRows}</tbody></table><p class=\"footer\">Route Planner · GomezLee Marketing</p></body></html>`;\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": []
|
|
} |