diff --git a/fulgencio-alertas-sheets-glm.json b/fulgencio-alertas-sheets-glm.json new file mode 100644 index 0000000..7bdc2ac --- /dev/null +++ b/fulgencio-alertas-sheets-glm.json @@ -0,0 +1,848 @@ +{ + "updatedAt": "2026-06-05T14:12:47.480Z", + "createdAt": "2026-05-15T14:28:58.553Z", + "id": "UzzVI2AKIXuZGgTL", + "name": "Fulgencio Alertas Sheets - GLM", + "description": null, + "active": true, + "isArchived": false, + "nodes": [ + { + "parameters": { + "rule": { + "interval": [ + { + "field": "weeks", + "triggerAtDay": [ + 5 + ], + "triggerAtHour": 9, + "triggerAtMinute": 30 + } + ] + } + }, + "type": "n8n-nodes-base.scheduleTrigger", + "typeVersion": 1.3, + "position": [ + -576, + -16 + ], + "id": "8ba8101b-5beb-4b95-ad54-240e1dc83c8f", + "name": "Schedule Trigger" + }, + { + "parameters": { + "documentId": { + "__rl": true, + "value": "1iy3lC3DS3ZfShb7d26iQOrPjx9s_3B-PC07XfYew7ng", + "mode": "list", + "cachedResultName": "BANCO DE PROPUESTAS DE CDC PARA FULGENCIO FUMADO", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1iy3lC3DS3ZfShb7d26iQOrPjx9s_3B-PC07XfYew7ng/edit?usp=drivesdk" + }, + "sheetName": { + "__rl": true, + "value": "gid=0", + "mode": "list", + "cachedResultName": "propuestas", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1iy3lC3DS3ZfShb7d26iQOrPjx9s_3B-PC07XfYew7ng/edit#gid=0" + }, + "filtersUI": { + "values": [] + }, + "options": {} + }, + "type": "n8n-nodes-base.googleSheets", + "typeVersion": 4.7, + "position": [ + -368, + -16 + ], + "id": "890d0701-b8fb-419e-bf9a-5cc781209c34", + "name": "Sheets - Leer propuestas", + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + } + }, + { + "parameters": { + "jsCode": "const rows = $input.all();\n\nconst SHEET_URL = 'https://docs.google.com/spreadsheets/d/1iy3lC3DS3ZfShb7d26iQOrPjx9s_3B-PC07XfYew7ng/edit?usp=sharing';\n\nconst MAX_FILAS_PRIORITARIAS = 40;\n\nconst fechaRevisionISO = new Date().toISOString();\n\nconst fechaRevisionVisible = new Intl.DateTimeFormat('es-DO', {\n timeZone: 'America/Santo_Domingo',\n year: 'numeric',\n month: '2-digit',\n day: '2-digit',\n hour: '2-digit',\n minute: '2-digit',\n hour12: true,\n}).format(new Date());\n\nconst columnasAValidar = [\n 'NOMBRE',\n 'TIPO DE ACCION',\n 'CLIENTE',\n 'MARCA',\n 'PAIS',\n 'CANAL',\n // APROBADA NO se valida. Ellos la completan luego manualmente.\n 'ETIQUETAS',\n 'AÑO',\n 'Enlace a la propuesta',\n 'Descripcion',\n];\n\nfunction clean(value) {\n return String(value ?? '').trim();\n}\n\nfunction norm(value) {\n return clean(value)\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .toUpperCase();\n}\n\nfunction escapeHtml(value) {\n return clean(value)\n .replace(/&/g, '&')\n .replace(//g, '>');\n}\n\nfunction filaTieneData(row) {\n const camposClave = [\n 'NOMBRE',\n 'TIPO DE ACCION',\n 'CLIENTE',\n 'MARCA',\n 'PAIS',\n 'Enlace a la propuesta',\n 'Descripcion',\n 'file_id',\n 'nombre_archivo',\n ];\n\n return camposClave.some(campo => clean(row[campo]));\n}\n\nfunction campoPendiente(value, campo) {\n const raw = clean(value);\n const v = norm(value);\n\n if (!raw) {\n return { pendiente: true, motivo: 'vacío' };\n }\n\n if (['UNDEFINED', 'NULL', 'N/A'].includes(v)) {\n return { pendiente: true, motivo: 'valor inválido' };\n }\n\n if (v === 'PENDIENTE') {\n return { pendiente: true, motivo: 'pendiente' };\n }\n\n if (v === 'SIN PAIS DEFINIDO') {\n return { pendiente: true, motivo: 'país sin definir' };\n }\n\n // Detecta si por error cayó un valor de APROBADA en otra columna.\n if (\n campo !== 'APROBADA' &&\n ['PENDIENTE DE APROBACION', 'APROBADA', 'NO APROBADA'].includes(v)\n ) {\n return { pendiente: true, motivo: 'valor de aprobación en columna incorrecta' };\n }\n\n const partes = v.split(',').map(x => x.trim());\n\n if (partes.includes('OTRO')) {\n return { pendiente: true, motivo: 'valor OTRO requiere revisión' };\n }\n\n return { pendiente: false, motivo: '' };\n}\n\nconst detalles = [];\nconst resumenPorFila = new Map();\nconst resumenPorCampo = new Map();\n\nrows.forEach((item, index) => {\n const row = item.json || {};\n\n if (!filaTieneData(row)) {\n return;\n }\n\n const fila =\n row.row_number ||\n row.__row_number ||\n row.__rowNumber ||\n row.rowNumber ||\n index + 2;\n\n const propuesta =\n clean(row.NOMBRE) ||\n clean(row.nombre_archivo) ||\n 'Sin nombre';\n\n const link = clean(row['Enlace a la propuesta']);\n\n for (const campo of columnasAValidar) {\n const resultado = campoPendiente(row[campo], campo);\n\n if (resultado.pendiente) {\n detalles.push({\n fecha_revision: fechaRevisionVisible,\n fecha_revision_iso: fechaRevisionISO,\n fila_origen: fila,\n propuesta,\n campo,\n valor_actual: clean(row[campo]),\n motivo: resultado.motivo,\n enlace_propuesta: link,\n });\n\n if (!resumenPorFila.has(fila)) {\n resumenPorFila.set(fila, {\n fila,\n propuesta,\n link,\n campos: [],\n });\n }\n\n resumenPorFila.get(fila).campos.push(campo);\n resumenPorCampo.set(campo, (resumenPorCampo.get(campo) || 0) + 1);\n }\n }\n});\n\nconst filasPendientes = Array.from(resumenPorFila.values());\nconst columnasResumen = Array.from(resumenPorCampo.entries())\n .map(([campo, total]) => ({ campo, total }))\n .sort((a, b) => b.total - a.total);\n\nif (detalles.length === 0) {\n return [\n {\n json: {\n hay_pendientes: false,\n fecha_revision: fechaRevisionVisible,\n total_filas_pendientes: 0,\n total_campos_pendientes: 0,\n mensaje: '✅ No hay propuestas pendientes de revisión.',\n chat_payload: {\n text: '✅ No hay propuestas pendientes de revisión.',\n },\n reporte_rows: [],\n },\n },\n ];\n}\n\nconst topFilas = filasPendientes\n .sort((a, b) => b.campos.length - a.campos.length)\n .slice(0, MAX_FILAS_PRIORITARIAS);\n\nconst topColumnas = columnasResumen.slice(0, 10);\n\nconst columnasTexto = topColumnas\n .map(c => `• ${escapeHtml(c.campo)}: ${c.total}`)\n .join('
');\n\nconst filasTexto = topFilas\n .map(f => {\n const camposTexto = f.campos.join(', ');\n\n return `• Fila ${f.fila} — ${escapeHtml(f.propuesta)}
${escapeHtml(camposTexto)}`;\n })\n .join('

');\n\nconst textoExtra =\n filasPendientes.length > MAX_FILAS_PRIORITARIAS\n ? `

Mostrando las primeras ${MAX_FILAS_PRIORITARIAS} filas prioritarias. Abre el Banco de Propuestas para ver el resto.`\n : '';\n\nconst chatPayload = {\n text: '🔔 *Revisión Pendiente - Banco de Propuestas*',\n cardsV2: [\n {\n cardId: 'fulgencio-alertas-pendientes',\n card: {\n header: {\n title: '📋 Datos Pendientes - Fulgencio',\n subtitle: `${filasPendientes.length} fila(s) requieren atención`,\n },\n sections: [\n {\n widgets: [\n {\n decoratedText: {\n topLabel: 'Fecha de revisión',\n text: `${escapeHtml(fechaRevisionVisible)}`,\n },\n },\n {\n decoratedText: {\n topLabel: 'Resumen',\n text: `${detalles.length} campo(s) pendientes o por revisar`,\n },\n },\n ],\n },\n {\n header: 'Columnas con más pendientes',\n widgets: [\n {\n textParagraph: {\n text: columnasTexto,\n },\n },\n ],\n },\n {\n header: `Filas Prioritarias - Primeras ${MAX_FILAS_PRIORITARIAS}`,\n widgets: [\n {\n textParagraph: {\n text: filasTexto + textoExtra,\n },\n },\n ],\n },\n {\n widgets: [\n {\n buttonList: {\n buttons: [\n {\n text: 'Abrir Banco de Propuestas',\n onClick: {\n openLink: {\n url: SHEET_URL,\n },\n },\n },\n ],\n },\n },\n ],\n },\n ],\n },\n },\n ],\n};\n\nreturn [\n {\n json: {\n hay_pendientes: true,\n fecha_revision: fechaRevisionVisible,\n total_filas_pendientes: filasPendientes.length,\n total_campos_pendientes: detalles.length,\n mensaje: '🔔 *Revisión Pendiente - Banco de Propuestas*',\n chat_payload: chatPayload,\n reporte_rows: detalles,\n },\n },\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -96, + -16 + ], + "id": "264afa9e-1714-42af-8784-a330df03b0d3", + "name": "Code - Detectar pendientes" + }, + { + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "a6b1c13d-7153-492b-88c8-23201d2ba19d", + "leftValue": "={{$json.hay_pendientes}}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "options": {} + }, + "type": "n8n-nodes-base.if", + "typeVersion": 2.3, + "position": [ + 176, + -16 + ], + "id": "3a272698-634d-456e-b5d8-4e6fb04af270", + "name": "IF - Hay pendientes" + }, + { + "parameters": { + "method": "POST", + "url": "https://chat.googleapis.com/v1/spaces/AAQADrFA_LI/messages?key=AIzaSyDdI0hCZtE6vySjMm-WEfRq3CPzqKqqsHI&token=OkRPuZYKTNfbs3GUq3i8b9OD7Q_SUzRaupzco1AQMy8", + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={{ $json.chat_payload }}", + "options": {} + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.4, + "position": [ + 624, + -192 + ], + "id": "456bce00-b0ee-4dea-90e8-57f7e8c0d18c", + "name": "HTTP Request - Enviar alerta" + }, + { + "parameters": { + "jsCode": "const reporte = $input.first().json.reporte_rows || [];\n\nreturn reporte.map(row => ({\n json: row\n}));" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 624, + 32 + ], + "id": "d513986c-b5b0-493a-a5ef-87655ed050f8", + "name": "Code - Expandir reporte pendientes" + }, + { + "parameters": { + "operation": "append", + "documentId": { + "__rl": true, + "value": "1LH6TIupvwUVpWu9pOnaWs-jt_xn1ojM_o8bCKURz12E", + "mode": "list", + "cachedResultName": "Fulgencio - Reporte de Pendientes", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1LH6TIupvwUVpWu9pOnaWs-jt_xn1ojM_o8bCKURz12E/edit?usp=drivesdk" + }, + "sheetName": { + "__rl": true, + "value": "gid=0", + "mode": "list", + "cachedResultName": "pendientes_revision", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1LH6TIupvwUVpWu9pOnaWs-jt_xn1ojM_o8bCKURz12E/edit#gid=0" + }, + "columns": { + "mappingMode": "defineBelow", + "value": { + "fecha_revision": "={{$json.fecha_revision}}", + "fecha_revision_iso": "={{$json.fecha_revision_iso}}", + "fila_origen": "={{$json.fila_origen}}", + "propuesta": "={{$json.propuesta}}", + "campo": "={{$json.campo}}", + "valor_actual": "={{$json.valor_actual}}", + "motivo": "={{$json.motivo}}", + "enlace_propuesta": "={{$json.enlace_propuesta}}" + }, + "matchingColumns": [], + "schema": [ + { + "id": "fecha_revision", + "displayName": "fecha_revision", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "fecha_revision_iso", + "displayName": "fecha_revision_iso", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "fila_origen", + "displayName": "fila_origen", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "propuesta", + "displayName": "propuesta", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "campo", + "displayName": "campo", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "valor_actual", + "displayName": "valor_actual", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "motivo", + "displayName": "motivo", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "enlace_propuesta", + "displayName": "enlace_propuesta", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + } + ], + "attemptToConvertTypes": false, + "convertFieldsToString": false + }, + "options": {} + }, + "type": "n8n-nodes-base.googleSheets", + "typeVersion": 4.7, + "position": [ + 880, + 32 + ], + "id": "4cffedf9-6dba-4b42-a895-78115034d3df", + "name": "Sheets - Guardar detalle pendientes", + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + } + }, + { + "parameters": { + "content": "⏰ Revisión programada\n\nEste flujo se ejecuta en horarios definidos para revisar el Google Sheet de propuestas.\n\nLee todas las propuestas registradas y busca campos vacíos, pendientes o que requieran revisión.", + "height": 352, + "width": 432 + }, + "type": "n8n-nodes-base.stickyNote", + "typeVersion": 1, + "position": [ + -640, + -176 + ], + "id": "84fe4bf9-ff8f-4426-86f3-fccb775fc261", + "name": "Sticky Note" + }, + { + "parameters": { + "content": "🔎 Validación de datos\n\nSe validan únicamente columnas de negocio:\nNOMBRE, TIPO DE ACCION, CLIENTE, MARCA, PAIS, CANAL, ETIQUETAS, AÑO, enlace y descripción.\n\nNo se validan columnas técnicas como APROBADA, AMBIENTE DE COMPRA ni TÁCTICA PROMOCIONAL.", + "height": 320, + "width": 512, + "color": 5 + }, + "type": "n8n-nodes-base.stickyNote", + "typeVersion": 1, + "position": [ + -128, + -176 + ], + "id": "b0a76191-979c-4378-beee-72b09345ae7e", + "name": "Sticky Note1" + }, + { + "parameters": { + "content": "📣 Alerta y detalle\n\nSi existen pendientes, se envía una tarjeta resumida al Google Chat de alertas. \n\nEl detalle completo se guarda en el Sheet de reporte para revisión, sin saturar el mensaje del chat. ", + "height": 544, + "width": 624, + "color": 4 + }, + "type": "n8n-nodes-base.stickyNote", + "typeVersion": 1, + "position": [ + 464, + -304 + ], + "id": "e43558a2-ef80-4ddf-8b8b-a9b721367e93", + "name": "Sticky Note2" + } + ], + "connections": { + "Schedule Trigger": { + "main": [ + [ + { + "node": "Sheets - Leer propuestas", + "type": "main", + "index": 0 + } + ] + ] + }, + "Sheets - Leer propuestas": { + "main": [ + [ + { + "node": "Code - Detectar pendientes", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Detectar pendientes": { + "main": [ + [ + { + "node": "IF - Hay pendientes", + "type": "main", + "index": 0 + } + ] + ] + }, + "IF - Hay pendientes": { + "main": [ + [ + { + "node": "HTTP Request - Enviar alerta", + "type": "main", + "index": 0 + }, + { + "node": "Code - Expandir reporte pendientes", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Expandir reporte pendientes": { + "main": [ + [ + { + "node": "Sheets - Guardar detalle pendientes", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "settings": { + "executionOrder": "v1", + "binaryMode": "separate", + "timeSavedMode": "fixed", + "errorWorkflow": "puF4LUczoSz3hcek", + "callerPolicy": "workflowsFromSameOwner", + "availableInMCP": false, + "timezone": "America/Santo_Domingo" + }, + "staticData": { + "node:Schedule Trigger": { + "recurrenceRules": [] + } + }, + "meta": { + "templateCredsSetupCompleted": true + }, + "versionId": "7725f0ba-17fa-4558-b70c-55209f25e234", + "activeVersionId": "7725f0ba-17fa-4558-b70c-55209f25e234", + "versionCounter": 216, + "triggerCount": 1, + "shared": [ + { + "updatedAt": "2026-05-15T14:28:58.560Z", + "createdAt": "2026-05-15T14:28:58.560Z", + "role": "workflow:owner", + "workflowId": "UzzVI2AKIXuZGgTL", + "projectId": "PJpTANzTXIFibWsW", + "project": { + "updatedAt": "2026-04-22T14:25:09.686Z", + "createdAt": "2026-04-22T14:22:54.790Z", + "id": "PJpTANzTXIFibWsW", + "name": "Isaac Aracena ", + "type": "personal", + "icon": null, + "description": null, + "creatorId": "0a88c0b1-928e-4412-896e-c5d1c99b2029" + } + } + ], + "tags": [], + "activeVersion": { + "updatedAt": "2026-06-05T14:13:22.000Z", + "createdAt": "2026-06-05T14:12:47.482Z", + "versionId": "7725f0ba-17fa-4558-b70c-55209f25e234", + "workflowId": "UzzVI2AKIXuZGgTL", + "nodes": [ + { + "parameters": { + "rule": { + "interval": [ + { + "field": "weeks", + "triggerAtDay": [ + 5 + ], + "triggerAtHour": 9, + "triggerAtMinute": 30 + } + ] + } + }, + "type": "n8n-nodes-base.scheduleTrigger", + "typeVersion": 1.3, + "position": [ + -576, + -16 + ], + "id": "8ba8101b-5beb-4b95-ad54-240e1dc83c8f", + "name": "Schedule Trigger" + }, + { + "parameters": { + "documentId": { + "__rl": true, + "value": "1iy3lC3DS3ZfShb7d26iQOrPjx9s_3B-PC07XfYew7ng", + "mode": "list", + "cachedResultName": "BANCO DE PROPUESTAS DE CDC PARA FULGENCIO FUMADO", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1iy3lC3DS3ZfShb7d26iQOrPjx9s_3B-PC07XfYew7ng/edit?usp=drivesdk" + }, + "sheetName": { + "__rl": true, + "value": "gid=0", + "mode": "list", + "cachedResultName": "propuestas", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1iy3lC3DS3ZfShb7d26iQOrPjx9s_3B-PC07XfYew7ng/edit#gid=0" + }, + "filtersUI": { + "values": [] + }, + "options": {} + }, + "type": "n8n-nodes-base.googleSheets", + "typeVersion": 4.7, + "position": [ + -368, + -16 + ], + "id": "890d0701-b8fb-419e-bf9a-5cc781209c34", + "name": "Sheets - Leer propuestas", + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + } + }, + { + "parameters": { + "jsCode": "const rows = $input.all();\n\nconst SHEET_URL = 'https://docs.google.com/spreadsheets/d/1iy3lC3DS3ZfShb7d26iQOrPjx9s_3B-PC07XfYew7ng/edit?usp=sharing';\n\nconst MAX_FILAS_PRIORITARIAS = 40;\n\nconst fechaRevisionISO = new Date().toISOString();\n\nconst fechaRevisionVisible = new Intl.DateTimeFormat('es-DO', {\n timeZone: 'America/Santo_Domingo',\n year: 'numeric',\n month: '2-digit',\n day: '2-digit',\n hour: '2-digit',\n minute: '2-digit',\n hour12: true,\n}).format(new Date());\n\nconst columnasAValidar = [\n 'NOMBRE',\n 'TIPO DE ACCION',\n 'CLIENTE',\n 'MARCA',\n 'PAIS',\n 'CANAL',\n // APROBADA NO se valida. Ellos la completan luego manualmente.\n 'ETIQUETAS',\n 'AÑO',\n 'Enlace a la propuesta',\n 'Descripcion',\n];\n\nfunction clean(value) {\n return String(value ?? '').trim();\n}\n\nfunction norm(value) {\n return clean(value)\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .toUpperCase();\n}\n\nfunction escapeHtml(value) {\n return clean(value)\n .replace(/&/g, '&')\n .replace(//g, '>');\n}\n\nfunction filaTieneData(row) {\n const camposClave = [\n 'NOMBRE',\n 'TIPO DE ACCION',\n 'CLIENTE',\n 'MARCA',\n 'PAIS',\n 'Enlace a la propuesta',\n 'Descripcion',\n 'file_id',\n 'nombre_archivo',\n ];\n\n return camposClave.some(campo => clean(row[campo]));\n}\n\nfunction campoPendiente(value, campo) {\n const raw = clean(value);\n const v = norm(value);\n\n if (!raw) {\n return { pendiente: true, motivo: 'vacío' };\n }\n\n if (['UNDEFINED', 'NULL', 'N/A'].includes(v)) {\n return { pendiente: true, motivo: 'valor inválido' };\n }\n\n if (v === 'PENDIENTE') {\n return { pendiente: true, motivo: 'pendiente' };\n }\n\n if (v === 'SIN PAIS DEFINIDO') {\n return { pendiente: true, motivo: 'país sin definir' };\n }\n\n // Detecta si por error cayó un valor de APROBADA en otra columna.\n if (\n campo !== 'APROBADA' &&\n ['PENDIENTE DE APROBACION', 'APROBADA', 'NO APROBADA'].includes(v)\n ) {\n return { pendiente: true, motivo: 'valor de aprobación en columna incorrecta' };\n }\n\n const partes = v.split(',').map(x => x.trim());\n\n if (partes.includes('OTRO')) {\n return { pendiente: true, motivo: 'valor OTRO requiere revisión' };\n }\n\n return { pendiente: false, motivo: '' };\n}\n\nconst detalles = [];\nconst resumenPorFila = new Map();\nconst resumenPorCampo = new Map();\n\nrows.forEach((item, index) => {\n const row = item.json || {};\n\n if (!filaTieneData(row)) {\n return;\n }\n\n const fila =\n row.row_number ||\n row.__row_number ||\n row.__rowNumber ||\n row.rowNumber ||\n index + 2;\n\n const propuesta =\n clean(row.NOMBRE) ||\n clean(row.nombre_archivo) ||\n 'Sin nombre';\n\n const link = clean(row['Enlace a la propuesta']);\n\n for (const campo of columnasAValidar) {\n const resultado = campoPendiente(row[campo], campo);\n\n if (resultado.pendiente) {\n detalles.push({\n fecha_revision: fechaRevisionVisible,\n fecha_revision_iso: fechaRevisionISO,\n fila_origen: fila,\n propuesta,\n campo,\n valor_actual: clean(row[campo]),\n motivo: resultado.motivo,\n enlace_propuesta: link,\n });\n\n if (!resumenPorFila.has(fila)) {\n resumenPorFila.set(fila, {\n fila,\n propuesta,\n link,\n campos: [],\n });\n }\n\n resumenPorFila.get(fila).campos.push(campo);\n resumenPorCampo.set(campo, (resumenPorCampo.get(campo) || 0) + 1);\n }\n }\n});\n\nconst filasPendientes = Array.from(resumenPorFila.values());\nconst columnasResumen = Array.from(resumenPorCampo.entries())\n .map(([campo, total]) => ({ campo, total }))\n .sort((a, b) => b.total - a.total);\n\nif (detalles.length === 0) {\n return [\n {\n json: {\n hay_pendientes: false,\n fecha_revision: fechaRevisionVisible,\n total_filas_pendientes: 0,\n total_campos_pendientes: 0,\n mensaje: '✅ No hay propuestas pendientes de revisión.',\n chat_payload: {\n text: '✅ No hay propuestas pendientes de revisión.',\n },\n reporte_rows: [],\n },\n },\n ];\n}\n\nconst topFilas = filasPendientes\n .sort((a, b) => b.campos.length - a.campos.length)\n .slice(0, MAX_FILAS_PRIORITARIAS);\n\nconst topColumnas = columnasResumen.slice(0, 10);\n\nconst columnasTexto = topColumnas\n .map(c => `• ${escapeHtml(c.campo)}: ${c.total}`)\n .join('
');\n\nconst filasTexto = topFilas\n .map(f => {\n const camposTexto = f.campos.join(', ');\n\n return `• Fila ${f.fila} — ${escapeHtml(f.propuesta)}
${escapeHtml(camposTexto)}`;\n })\n .join('

');\n\nconst textoExtra =\n filasPendientes.length > MAX_FILAS_PRIORITARIAS\n ? `

Mostrando las primeras ${MAX_FILAS_PRIORITARIAS} filas prioritarias. Abre el Banco de Propuestas para ver el resto.`\n : '';\n\nconst chatPayload = {\n text: '🔔 *Revisión Pendiente - Banco de Propuestas*',\n cardsV2: [\n {\n cardId: 'fulgencio-alertas-pendientes',\n card: {\n header: {\n title: '📋 Datos Pendientes - Fulgencio',\n subtitle: `${filasPendientes.length} fila(s) requieren atención`,\n },\n sections: [\n {\n widgets: [\n {\n decoratedText: {\n topLabel: 'Fecha de revisión',\n text: `${escapeHtml(fechaRevisionVisible)}`,\n },\n },\n {\n decoratedText: {\n topLabel: 'Resumen',\n text: `${detalles.length} campo(s) pendientes o por revisar`,\n },\n },\n ],\n },\n {\n header: 'Columnas con más pendientes',\n widgets: [\n {\n textParagraph: {\n text: columnasTexto,\n },\n },\n ],\n },\n {\n header: `Filas Prioritarias - Primeras ${MAX_FILAS_PRIORITARIAS}`,\n widgets: [\n {\n textParagraph: {\n text: filasTexto + textoExtra,\n },\n },\n ],\n },\n {\n widgets: [\n {\n buttonList: {\n buttons: [\n {\n text: 'Abrir Banco de Propuestas',\n onClick: {\n openLink: {\n url: SHEET_URL,\n },\n },\n },\n ],\n },\n },\n ],\n },\n ],\n },\n },\n ],\n};\n\nreturn [\n {\n json: {\n hay_pendientes: true,\n fecha_revision: fechaRevisionVisible,\n total_filas_pendientes: filasPendientes.length,\n total_campos_pendientes: detalles.length,\n mensaje: '🔔 *Revisión Pendiente - Banco de Propuestas*',\n chat_payload: chatPayload,\n reporte_rows: detalles,\n },\n },\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -96, + -16 + ], + "id": "264afa9e-1714-42af-8784-a330df03b0d3", + "name": "Code - Detectar pendientes" + }, + { + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "a6b1c13d-7153-492b-88c8-23201d2ba19d", + "leftValue": "={{$json.hay_pendientes}}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "options": {} + }, + "type": "n8n-nodes-base.if", + "typeVersion": 2.3, + "position": [ + 176, + -16 + ], + "id": "3a272698-634d-456e-b5d8-4e6fb04af270", + "name": "IF - Hay pendientes" + }, + { + "parameters": { + "method": "POST", + "url": "https://chat.googleapis.com/v1/spaces/AAQADrFA_LI/messages?key=AIzaSyDdI0hCZtE6vySjMm-WEfRq3CPzqKqqsHI&token=OkRPuZYKTNfbs3GUq3i8b9OD7Q_SUzRaupzco1AQMy8", + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={{ $json.chat_payload }}", + "options": {} + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.4, + "position": [ + 624, + -192 + ], + "id": "456bce00-b0ee-4dea-90e8-57f7e8c0d18c", + "name": "HTTP Request - Enviar alerta" + }, + { + "parameters": { + "jsCode": "const reporte = $input.first().json.reporte_rows || [];\n\nreturn reporte.map(row => ({\n json: row\n}));" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 624, + 32 + ], + "id": "d513986c-b5b0-493a-a5ef-87655ed050f8", + "name": "Code - Expandir reporte pendientes" + }, + { + "parameters": { + "operation": "append", + "documentId": { + "__rl": true, + "value": "1LH6TIupvwUVpWu9pOnaWs-jt_xn1ojM_o8bCKURz12E", + "mode": "list", + "cachedResultName": "Fulgencio - Reporte de Pendientes", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1LH6TIupvwUVpWu9pOnaWs-jt_xn1ojM_o8bCKURz12E/edit?usp=drivesdk" + }, + "sheetName": { + "__rl": true, + "value": "gid=0", + "mode": "list", + "cachedResultName": "pendientes_revision", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1LH6TIupvwUVpWu9pOnaWs-jt_xn1ojM_o8bCKURz12E/edit#gid=0" + }, + "columns": { + "mappingMode": "defineBelow", + "value": { + "fecha_revision": "={{$json.fecha_revision}}", + "fecha_revision_iso": "={{$json.fecha_revision_iso}}", + "fila_origen": "={{$json.fila_origen}}", + "propuesta": "={{$json.propuesta}}", + "campo": "={{$json.campo}}", + "valor_actual": "={{$json.valor_actual}}", + "motivo": "={{$json.motivo}}", + "enlace_propuesta": "={{$json.enlace_propuesta}}" + }, + "matchingColumns": [], + "schema": [ + { + "id": "fecha_revision", + "displayName": "fecha_revision", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "fecha_revision_iso", + "displayName": "fecha_revision_iso", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "fila_origen", + "displayName": "fila_origen", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "propuesta", + "displayName": "propuesta", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "campo", + "displayName": "campo", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "valor_actual", + "displayName": "valor_actual", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "motivo", + "displayName": "motivo", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "enlace_propuesta", + "displayName": "enlace_propuesta", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + } + ], + "attemptToConvertTypes": false, + "convertFieldsToString": false + }, + "options": {} + }, + "type": "n8n-nodes-base.googleSheets", + "typeVersion": 4.7, + "position": [ + 880, + 32 + ], + "id": "4cffedf9-6dba-4b42-a895-78115034d3df", + "name": "Sheets - Guardar detalle pendientes", + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + } + }, + { + "parameters": { + "content": "⏰ Revisión programada\n\nEste flujo se ejecuta en horarios definidos para revisar el Google Sheet de propuestas.\n\nLee todas las propuestas registradas y busca campos vacíos, pendientes o que requieran revisión.", + "height": 352, + "width": 432 + }, + "type": "n8n-nodes-base.stickyNote", + "typeVersion": 1, + "position": [ + -640, + -176 + ], + "id": "84fe4bf9-ff8f-4426-86f3-fccb775fc261", + "name": "Sticky Note" + }, + { + "parameters": { + "content": "🔎 Validación de datos\n\nSe validan únicamente columnas de negocio:\nNOMBRE, TIPO DE ACCION, CLIENTE, MARCA, PAIS, CANAL, ETIQUETAS, AÑO, enlace y descripción.\n\nNo se validan columnas técnicas como APROBADA, AMBIENTE DE COMPRA ni TÁCTICA PROMOCIONAL.", + "height": 320, + "width": 512, + "color": 5 + }, + "type": "n8n-nodes-base.stickyNote", + "typeVersion": 1, + "position": [ + -128, + -176 + ], + "id": "b0a76191-979c-4378-beee-72b09345ae7e", + "name": "Sticky Note1" + }, + { + "parameters": { + "content": "📣 Alerta y detalle\n\nSi existen pendientes, se envía una tarjeta resumida al Google Chat de alertas. \n\nEl detalle completo se guarda en el Sheet de reporte para revisión, sin saturar el mensaje del chat. ", + "height": 544, + "width": 624, + "color": 4 + }, + "type": "n8n-nodes-base.stickyNote", + "typeVersion": 1, + "position": [ + 464, + -304 + ], + "id": "e43558a2-ef80-4ddf-8b8b-a9b721367e93", + "name": "Sticky Note2" + } + ], + "connections": { + "Schedule Trigger": { + "main": [ + [ + { + "node": "Sheets - Leer propuestas", + "type": "main", + "index": 0 + } + ] + ] + }, + "Sheets - Leer propuestas": { + "main": [ + [ + { + "node": "Code - Detectar pendientes", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Detectar pendientes": { + "main": [ + [ + { + "node": "IF - Hay pendientes", + "type": "main", + "index": 0 + } + ] + ] + }, + "IF - Hay pendientes": { + "main": [ + [ + { + "node": "HTTP Request - Enviar alerta", + "type": "main", + "index": 0 + }, + { + "node": "Code - Expandir reporte pendientes", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Expandir reporte pendientes": { + "main": [ + [ + { + "node": "Sheets - Guardar detalle pendientes", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "authors": "Isaac Aracena", + "name": "Version 7725f0ba", + "description": "", + "autosaved": false, + "workflowPublishHistory": [ + { + "createdAt": "2026-06-05T14:12:48.850Z", + "id": 1136, + "workflowId": "UzzVI2AKIXuZGgTL", + "versionId": "7725f0ba-17fa-4558-b70c-55209f25e234", + "event": "activated", + "userId": "0a88c0b1-928e-4412-896e-c5d1c99b2029" + }, + { + "createdAt": "2026-06-05T14:13:22.445Z", + "id": 1138, + "workflowId": "UzzVI2AKIXuZGgTL", + "versionId": "7725f0ba-17fa-4558-b70c-55209f25e234", + "event": "activated", + "userId": "0a88c0b1-928e-4412-896e-c5d1c99b2029" + }, + { + "createdAt": "2026-06-05T14:13:20.019Z", + "id": 1137, + "workflowId": "UzzVI2AKIXuZGgTL", + "versionId": "7725f0ba-17fa-4558-b70c-55209f25e234", + "event": "deactivated", + "userId": "0a88c0b1-928e-4412-896e-c5d1c99b2029" + } + ] + } +} \ No newline at end of file