403 lines
18 KiB
JSON
403 lines
18 KiB
JSON
{
|
|
"name": "OptimizadorRutasPDV TEST - Fixed v2",
|
|
"nodes": [
|
|
{
|
|
"parameters": {
|
|
"httpMethod": "POST",
|
|
"path": "calcular-camino",
|
|
"responseMode": "responseNode",
|
|
"options": {}
|
|
},
|
|
"type": "n8n-nodes-base.webhook",
|
|
"typeVersion": 2.1,
|
|
"position": [
|
|
-736,
|
|
544
|
|
],
|
|
"id": "f84fe2f2-2875-4e5c-ad99-4b4cc7a2c8d2",
|
|
"name": "Webhook",
|
|
"webhookId": "01fc4a9c-bad3-42b8-9f60-b87d91302502"
|
|
},
|
|
{
|
|
"parameters": {
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "strict",
|
|
"version": 3
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "c1",
|
|
"leftValue": "={{$binary.pdv_file.fileName}}",
|
|
"rightValue": "",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "exists",
|
|
"singleValue": true
|
|
}
|
|
},
|
|
{
|
|
"id": "c2",
|
|
"leftValue": "={{$binary.usuarios_file.fileName}}",
|
|
"rightValue": "",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "exists",
|
|
"singleValue": true
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"options": {}
|
|
},
|
|
"type": "n8n-nodes-base.if",
|
|
"typeVersion": 2.3,
|
|
"position": [
|
|
-496,
|
|
544
|
|
],
|
|
"id": "5ecfc62e-849c-442b-90b8-4885494e3820",
|
|
"name": "Validate Files"
|
|
},
|
|
{
|
|
"parameters": {
|
|
"respondWith": "json",
|
|
"responseBody": "{\n \"error\": \"Debe subir ambos archivos: PDV y Usuarios\"\n}",
|
|
"options": {}
|
|
},
|
|
"type": "n8n-nodes-base.respondToWebhook",
|
|
"typeVersion": 1.5,
|
|
"position": [
|
|
-496,
|
|
768
|
|
],
|
|
"id": "2c0adaf5-050d-4c2e-a509-ed77cc5a6784",
|
|
"name": "Error: Missing Files"
|
|
},
|
|
{
|
|
"parameters": {
|
|
"operation": "xlsx",
|
|
"binaryPropertyName": "pdv_file",
|
|
"options": {
|
|
"headerRow": true
|
|
}
|
|
},
|
|
"type": "n8n-nodes-base.extractFromFile",
|
|
"typeVersion": 1.1,
|
|
"position": [
|
|
-160,
|
|
416
|
|
],
|
|
"id": "90f15538-1ae9-454b-9038-b3d3cbf39f00",
|
|
"name": "Extract PDV"
|
|
},
|
|
{
|
|
"parameters": {
|
|
"operation": "xlsx",
|
|
"binaryPropertyName": "usuarios_file",
|
|
"options": {
|
|
"headerRow": true
|
|
}
|
|
},
|
|
"type": "n8n-nodes-base.extractFromFile",
|
|
"typeVersion": 1.1,
|
|
"position": [
|
|
-160,
|
|
640
|
|
],
|
|
"id": "a8a72a9e-7e38-486a-9db2-7efb130c6963",
|
|
"name": "Extract Usuarios"
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "const allRows = $input.all().map(item => item.json);\nif (!allRows.length) {\n return [{ json: { success: false, data: [], message: 'PDV: Archivo vacío' } }];\n}\nconst keys = new Set(allRows.flatMap(r => Object.keys(r)));\nconst required = ['ID','Nombre del PDV','Segmentación','Latitud','Longitud','Visitas semanales','Duración visita(horas)','Prioridad'];\nconst missing = required.filter(c => !keys.has(c));\nlet message = '';\nlet success = true;\nif (missing.length) {\n success = false;\n message = `PDV: Columnas faltantes: ${missing.join(', ')}`;\n}\nif (allRows.length > 2000) {\n success = false;\n message = 'PDV: Máximo 2000 filas permitidas';\n}\nreturn [{ json: { success, data: $input.all(), message } }];"
|
|
},
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
80,
|
|
416
|
|
],
|
|
"id": "e7acdf1f-4741-4189-91b9-30a90d42b531",
|
|
"name": "Validate PDV"
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "const allRows = $input.all().map(item => item.json);\nif (!allRows.length) {\n return [{ json: { success: false, data: [], message: 'Usuarios: Archivo vacío' } }];\n}\nconst keys = new Set(allRows.flatMap(r => Object.keys(r)));\nconst required = ['ID','Nombre del empleado','Latitud','Longitud','Horas por día usuario','Horas por semana usuario'];\nconst missing = required.filter(c => !keys.has(c));\nlet message = '';\nlet success = true;\nif (missing.length) {\n success = false;\n message = `Usuarios: Columnas faltantes: ${missing.join(', ')}`;\n}\nif (allRows.length > 50) {\n success = false;\n message = 'Usuarios: Máximo 50 filas permitidas';\n}\nreturn [{ json: { success, data: $input.all(), message } }];"
|
|
},
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
80,
|
|
640
|
|
],
|
|
"id": "0d734c5f-bf19-4eaa-9d82-16657523b53c",
|
|
"name": "Validate Usuarios"
|
|
},
|
|
{
|
|
"parameters": {},
|
|
"type": "n8n-nodes-base.merge",
|
|
"typeVersion": 3.2,
|
|
"position": [
|
|
336,
|
|
544
|
|
],
|
|
"id": "e13702a2-1b82-4a1f-bba6-e8880c8346aa",
|
|
"name": "Merge Validations"
|
|
},
|
|
{
|
|
"parameters": {
|
|
"respondWith": "json",
|
|
"responseBody": "={{ JSON.stringify({ error: $json[0].message || $json[1].message || 'Error de validación' }) }}",
|
|
"options": {}
|
|
},
|
|
"type": "n8n-nodes-base.respondToWebhook",
|
|
"typeVersion": 1.5,
|
|
"position": [
|
|
576,
|
|
768
|
|
],
|
|
"id": "d496c883-3206-47f5-89f4-f829ed167845",
|
|
"name": "Error: Validation Failed"
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "const items = $input.all();\n\nconst pdvResult = items[0]?.json;\nconst userResult = items[1]?.json;\n\nif (!pdvResult?.success) {\n return [{ json: { error: pdvResult?.message || 'Error validación PDVs' } }];\n}\nif (!userResult?.success) {\n return [{ json: { error: userResult?.message || 'Error validación Usuarios' } }];\n}\n\nconst pdvsRaw = pdvResult.data.map(item => item.json);\nconst usuariosRaw = userResult.data.map(item => item.json);\n\nif (!pdvsRaw.length || !usuariosRaw.length) {\n return [{ json: { error: 'Archivos vacíos o sin datos válidos' } }];\n}\n\nconst VELOCIDAD_DESPLAZAMIENTO = 46;\nconst asignaciones = [];\nconst PDVnoAsignados = [];\n\nfunction calcularDistancia(lat1, lon1, lat2, lon2) {\n const R = 6371;\n const dLat = (lat2 - lat1) * Math.PI / 180;\n const dLon = (lon2 - lon1) * Math.PI / 180;\n const a = Math.sin(dLat/2)**2 + Math.cos(lat1*Math.PI/180) * Math.cos(lat2*Math.PI/180) * Math.sin(dLon/2)**2;\n return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));\n}\n\nfunction formatHeader(value) {\n return value.toLowerCase()\n .normalize('NFD').replace(/[\\u0300-\\u036f]/g, '')\n .replace(/[^a-z0-9 ]/gi, '').trim().replace(/\\s+/g, '_');\n}\n\nfunction cleanNum(val) {\n if (val === null || val === undefined) return 0;\n if (typeof val === 'number') return val;\n return parseFloat(String(val).replace(/[^0-9.\\-]/g, '')) || 0;\n}\n\nfunction renameHeaders(rows) {\n return rows.map(row => {\n const newRow = {};\n for (const key in row) {\n const newKey = formatHeader(key);\n let val = row[key];\n if (typeof val === 'string') val = val.trim();\n newRow[newKey] = val;\n }\n return newRow;\n });\n}\n\nconst pdvs = renameHeaders(pdvsRaw);\nconst usuarios = renameHeaders(usuariosRaw);\n\n// Mapear columnas del Excel a nombres internos\nfunction mapPdv(p) {\n return {\n id: p.id,\n nombre_del_pdv: p.nombre_del_pdv,\n segmentacion: p.segmentacion || 'D',\n latitud: cleanNum(p.latitud),\n longitud: cleanNum(p.longitud),\n visitas_semanales: cleanNum(p.visitas_semanales),\n duracion_visita_horas: cleanNum(p.duracion_visitahoras),\n prioridad: cleanNum(p.prioridad),\n };\n}\n\nfunction mapUsuario(u) {\n return {\n id: u.id,\n nombre_del_empleado: u.nombre_del_empleado,\n latitud: cleanNum(u.latitud),\n longitud: cleanNum(u.longitud),\n horas_por_dia_usuario: cleanNum(u.horas_por_dia_usuario),\n horas_por_semana_usuario: cleanNum(u.horas_por_semana_usuario),\n };\n}\n\nconst pdvsMapeados = pdvs.map(mapPdv);\nconst usuariosMapeados = usuarios.map(mapUsuario);\n\nfunction asignarPdvUsuarios() {\n const orden = ['AA', 'A', 'B', 'C', 'D'];\n const horasDisp = {};\n const horasNecesita = {};\n\n for (const u of usuariosMapeados) horasDisp[u.id] = u.horas_por_semana_usuario;\n for (const p of pdvsMapeados) horasNecesita[p.id] = p.duracion_visita_horas * p.visitas_semanales;\n\n const pdvsSort = [...pdvsMapeados].sort((a, b) => {\n const idxA = orden.indexOf(a.segmentacion);\n const idxB = orden.indexOf(b.segmentacion);\n return idxA - idxB || a.prioridad - b.prioridad;\n });\n\n for (const pdv of pdvsSort) {\n if (horasNecesita[pdv.id] <= 0) {\n PDVnoAsignados.push({ pdv_id: pdv.id, pdv_nombre: pdv.nombre_del_pdv, minimo_horas_semana: horasNecesita[pdv.id], latitud: pdv.latitud, longitud: pdv.longitud, motivo: 'Sin horas requeridas' });\n continue;\n }\n\n let pdvAsignado = false;\n const candidatos = usuariosMapeados\n .map(u => ({\n usuario: u,\n distancia: calcularDistancia(u.latitud, u.longitud, pdv.latitud, pdv.longitud)\n }))\n .filter(c => c.distancia <= 50)\n .sort((a, b) => a.distancia - b.distancia);\n\n for (const cand of candidatos) {\n if (pdvAsignado) break;\n const u = cand.usuario;\n const horasUsuario = horasDisp[u.id];\n const horasPdv = horasNecesita[pdv.id];\n const horasAsignadas = Math.min(horasUsuario, horasPdv);\n\n if (horasAsignadas <= 0) continue;\n\n asignaciones.push({\n usuario_id: u.id,\n pdv_id: pdv.id,\n distancia: cand.distancia,\n horas_asignadas: horasAsignadas,\n horasDiariasUsuario: u.horas_por_dia_usuario\n });\n horasDisp[u.id] -= horasAsignadas;\n horasNecesita[pdv.id] -= horasAsignadas;\n pdvAsignado = true;\n }\n\n if (!pdvAsignado) {\n PDVnoAsignados.push({ pdv_id: pdv.id, pdv_nombre: pdv.nombre_del_pdv, minimo_horas_semana: pdv.duracion_visita_horas * pdv.visitas_semanales, latitud: pdv.latitud, longitud: pdv.longitud, motivo: 'Sin usuario disponible dentro de 50km' });\n }\n }\n}\n\nfunction asignarDiasHorasVisita() {\n const DIAS = ['Lunes', 'Martes', 'Miercoles', 'Jueves', 'Viernes'];\n const visitas = [];\n for (const a of asignaciones) {\n let horasRestantes = a.horas_asignadas;\n for (let i = 0; i < DIAS.length; i++) {\n const tiempoDesplazamiento = a.distancia / VELOCIDAD_DESPLAZAMIENTO;\n const horasDia = horasRestantes / (DIAS.length - i);\n if (horasDia <= a.horasDiariasUsuario) {\n visitas.push({\n usuario_id: a.usuario_id,\n pdv_id: a.pdv_id,\n dia: DIAS[i],\n horas_trabajo: Number(horasDia.toFixed(2)),\n horas_desplazamiento: Number(tiempoDesplazamiento.toFixed(2)),\n distancia: Number(a.distancia.toFixed(2)),\n });\n horasRestantes -= horasDia;\n } else {\n PDVnoAsignados.push({ pdv_id: a.pdv_id, pdv_nombre: '', minimo_horas_semana: 0, latitud: 0, longitud: 0, motivo: 'Excedió horas por día del usuario' });\n }\n }\n }\n return visitas;\n}\n\nasignarPdvUsuarios();\nconst visitas = asignarDiasHorasVisita();\n\nconst usuariosById = Object.fromEntries(usuariosMapeados.map(u => [u.id, u]));\nconst pdvsById = Object.fromEntries(pdvsMapeados.map(p => [p.id, p]));\n\nconst resultadoFinal = visitas.map(v => {\n const u = usuariosById[v.usuario_id];\n const p = pdvsById[v.pdv_id];\n if (!u || !p) return null;\n return {\n usuario_id: v.usuario_id,\n nombre_usuario: u.nombre_del_empleado,\n latitud_usuario: u.latitud,\n longitud_usuario: u.longitud,\n hora_laboral_semanal_usuario: u.horas_por_semana_usuario,\n pdv_id: v.pdv_id,\n nombre_pdv: p.nombre_del_pdv,\n latitud_pdv: p.latitud,\n longitud_pdv: p.longitud,\n hora_minima_semanal_pdv: p.duracion_visita_horas * p.visitas_semanales,\n dia: v.dia,\n horas_trabajo: v.horas_trabajo,\n horas_desplazamiento: v.horas_desplazamiento,\n distancia: v.distancia,\n };\n}).filter(Boolean);\n\nconst a = resultadoFinal.map(r => ({ json: r }));\nconst bFiltrado = PDVnoAsignados.filter(pdv => !a.some(x => x.json.pdv_id === pdv.pdv_id));\n\nreturn [{ assigned: a }, { noAssigned: bFiltrado }];"
|
|
},
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
816,
|
|
544
|
|
],
|
|
"id": "e9fe9826-4755-497e-ae6e-274e7c1d4f66",
|
|
"name": "Optimize Routes"
|
|
},
|
|
{
|
|
"parameters": {
|
|
"respondWith": "allIncomingItems",
|
|
"options": {}
|
|
},
|
|
"type": "n8n-nodes-base.respondToWebhook",
|
|
"typeVersion": 1.5,
|
|
"position": [
|
|
1024,
|
|
544
|
|
],
|
|
"id": "b024c203-1b36-4f7c-97bb-3923481b7a9b",
|
|
"name": "Respond Success"
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "const items = $input.all();\nconst pdv = items[0]; // Validate PDV output\nconst usuarios = items[1]; // Validate Usuarios output\n\nconst pdvOk = pdv?.json?.success === true;\nconst usuariosOk = usuarios?.json?.success === true;\n\nif (pdvOk && usuariosOk) {\n // Both passed → send to Optimize Routes (output 0)\n return items;\n} else {\n // Failed → send to Error (output 1)\n const msgs = [];\n if (!pdvOk) msgs.push(pdv?.json?.message || 'Error PDV');\n if (!usuariosOk) msgs.push(usuarios?.json?.message || 'Error Usuarios');\n throw new Error(msgs.join(' | '));\n}"
|
|
},
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
576,
|
|
544
|
|
],
|
|
"id": "27eb73de-4b08-4c44-97de-4d122a98749c",
|
|
"name": "Code in JavaScript",
|
|
"onError": "continueErrorOutput"
|
|
}
|
|
],
|
|
"pinData": {
|
|
"Webhook": [
|
|
{
|
|
"json": {
|
|
"headers": {
|
|
"host": "agenteit.digitalcompass.agency",
|
|
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/151.0.0.0 Safari/537.36",
|
|
"content-length": "19829",
|
|
"accept": "application/json, text/plain, */*",
|
|
"accept-encoding": "gzip, deflate, br, zstd",
|
|
"accept-language": "en-US,en;q=0.6",
|
|
"content-type": "multipart/form-data; boundary=----WebKitFormBoundaryeTQ0nhU3IDSv0jrS",
|
|
"origin": "http://localhost:4200",
|
|
"priority": "u=1, i",
|
|
"referer": "http://localhost:4200/",
|
|
"sec-ch-ua": "\"Not=A?Brand\";v=\"99\", \"Brave\";v=\"151\", \"Chromium\";v=\"151\"",
|
|
"sec-ch-ua-mobile": "?0",
|
|
"sec-ch-ua-platform": "\"Windows\"",
|
|
"sec-fetch-dest": "empty",
|
|
"sec-fetch-mode": "cors",
|
|
"sec-fetch-site": "cross-site",
|
|
"sec-gpc": "1",
|
|
"x-forwarded-for": "152.0.172.53",
|
|
"x-forwarded-host": "agenteit.digitalcompass.agency",
|
|
"x-forwarded-port": "443",
|
|
"x-forwarded-proto": "https",
|
|
"x-forwarded-server": "07b4f09d2c65",
|
|
"x-real-ip": "152.0.172.53"
|
|
},
|
|
"params": {},
|
|
"query": {},
|
|
"body": {},
|
|
"webhookUrl": "https://agenteit.digitalcompass.agency/webhook/calcular-camino",
|
|
"executionMode": "production"
|
|
},
|
|
"binary": {
|
|
"pdv_file": {
|
|
"mimeType": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
"fileExtension": "xlsx",
|
|
"data": "filesystem-v2",
|
|
"fileName": "Plantilla_PDV (2).xlsx",
|
|
"id": "filesystem-v2:workflows/IkKMqgb7uZuRk9d9/executions/48469/binary_data/48a414e1-9c74-469b-9bec-04283032f093",
|
|
"fileSize": "9.53 kB",
|
|
"bytes": 9528
|
|
},
|
|
"usuarios_file": {
|
|
"mimeType": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
"fileExtension": "xlsx",
|
|
"data": "filesystem-v2",
|
|
"fileName": "Plantilla_Usuarios (2).xlsx",
|
|
"id": "filesystem-v2:workflows/IkKMqgb7uZuRk9d9/executions/48469/binary_data/6553aefd-4d40-45dd-9cdd-bfb1b1d1ec84",
|
|
"fileSize": "9.82 kB",
|
|
"bytes": 9825
|
|
}
|
|
},
|
|
"pairedItem": {
|
|
"item": 0
|
|
}
|
|
}
|
|
]
|
|
},
|
|
"connections": {
|
|
"Webhook": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Validate Files",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Validate Files": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Extract PDV",
|
|
"type": "main",
|
|
"index": 0
|
|
},
|
|
{
|
|
"node": "Extract Usuarios",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Error: Missing Files",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Extract PDV": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Validate PDV",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Extract Usuarios": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Validate Usuarios",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Validate PDV": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Merge Validations",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Validate Usuarios": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Merge Validations",
|
|
"type": "main",
|
|
"index": 1
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Merge Validations": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Code in JavaScript",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Optimize Routes": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Respond Success",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Code in JavaScript": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Optimize Routes",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Error: Validation Failed",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
}
|
|
},
|
|
"active": true,
|
|
"settings": {
|
|
"executionOrder": "v1",
|
|
"binaryMode": "separate",
|
|
"timeSavedMode": "fixed",
|
|
"callerPolicy": "workflowsFromSameOwner",
|
|
"availableInMCP": true
|
|
},
|
|
"versionId": "f0dcd3f6-8043-4c22-9ca9-a477ae69db53",
|
|
"meta": {
|
|
"instanceId": "b4b77b17af092830e794eef639ce2f6d7daccf7eddc075060b03b3b6545aac70"
|
|
},
|
|
"id": "IkKMqgb7uZuRk9d9",
|
|
"tags": []
|
|
} |