From f2d3ef52c6dab20cba9393e563abab12254f0e18 Mon Sep 17 00:00:00 2001 From: Isaac_Aracena Date: Mon, 27 Jul 2026 16:32:10 +0000 Subject: [PATCH] =?UTF-8?q?A=C3=B1adir=20ENVIO=20FINAL=20-=20Detalles=20de?= =?UTF-8?q?=20finiquito=20a=20colaboradores.json?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...Detalles de finiquito a colaboradores.json | 398 ++++++++++++++++++ 1 file changed, 398 insertions(+) create mode 100644 ENVIO FINAL - Detalles de finiquito a colaboradores.json diff --git a/ENVIO FINAL - Detalles de finiquito a colaboradores.json b/ENVIO FINAL - Detalles de finiquito a colaboradores.json new file mode 100644 index 0000000..c7b912d --- /dev/null +++ b/ENVIO FINAL - Detalles de finiquito a colaboradores.json @@ -0,0 +1,398 @@ +{ + "nodes": [ + { + "parameters": {}, + "id": "e909153a-a0ed-454b-8795-60483cd6c1cd", + "name": "Manual Trigger", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [ + 368, + 2000 + ] + }, + { + "parameters": { + "url": "https://sheets.googleapis.com/v4/spreadsheets/1Y9XhRjSSxtymV7Nm0gYbC5lLuGD2zPTc7IVb8PxoxjY/values/CONTROL_ENVIOS!A:L", + "authentication": "predefinedCredentialType", + "nodeCredentialType": "googleSheetsOAuth2Api", + "options": {} + }, + "id": "174cb623-eaf2-4839-a519-80e6cfbac019", + "name": "Leer CONTROL_ENVIOS", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 624, + 2000 + ], + "retryOnFail": true, + "maxTries": 3, + "waitBetweenTries": 5000, + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + } + }, + { + "parameters": { + "jsCode": "const response = $input.first().json;\nconst values = response.values || [];\n\nif (!Array.isArray(values) || values.length < 2) {\n throw new Error('No se recibieron filas desde CONTROL_ENVIOS. Revisa el ID del Google Sheet y el nombre de la hoja.');\n}\n\nfunction normalizeKey(value) {\n return String(value || '')\n .trim()\n .toLowerCase()\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '');\n}\n\nfunction normalizeText(value) {\n return String(value || '')\n .trim()\n .toUpperCase()\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '');\n}\n\nfunction colToLetter(colNumber) {\n let temp = '';\n let letter = '';\n\n while (colNumber > 0) {\n temp = (colNumber - 1) % 26;\n letter = String.fromCharCode(temp + 65) + letter;\n colNumber = (colNumber - temp - 1) / 26;\n }\n\n return letter;\n}\n\nfunction getValue(row, possibleKeys) {\n for (const key of possibleKeys) {\n const direct = row[key];\n const normalized = row[normalizeKey(key)];\n\n if (direct !== undefined && direct !== null && String(direct).trim() !== '') {\n return String(direct).trim();\n }\n\n if (normalized !== undefined && normalized !== null && String(normalized).trim() !== '') {\n return String(normalized).trim();\n }\n }\n\n return '';\n}\n\nconst headers = values[0].map(header => String(header || '').trim());\nconst headerMap = {};\n\nheaders.forEach((header, index) => {\n headerMap[normalizeKey(header)] = {\n name: header,\n index,\n letter: colToLetter(index + 1)\n };\n});\n\nfunction requireHeader(nameOptions, label) {\n for (const option of nameOptions) {\n const normalized = normalizeKey(option);\n if (headerMap[normalized]) {\n return headerMap[normalized];\n }\n }\n\n throw new Error(`No encontré la columna requerida: ${label}.`);\n}\n\nconst colNo = requireHeader(['NO.', 'NO', 'No'], 'NO.');\nconst colHoja = requireHeader(['Hoja'], 'Hoja');\nconst colNombre = requireHeader(['Nombre'], 'Nombre');\nconst colEmail = requireHeader(['Email personal', 'Email Personal', 'Correo personal'], 'Email personal');\nconst colPdfFileId = requireHeader(['PDF File ID'], 'PDF File ID');\nconst colPdfUrl = requireHeader(['PDF URL'], 'PDF URL');\nconst colEstado = requireHeader(['Estado envío', 'Estado Envio'], 'Estado envío');\nconst colObservacion = requireHeader(['Observación', 'Observacion'], 'Observación');\n\nconst rows = values.slice(1).map((row, index) => {\n const obj = {\n __rowNumber: index + 2\n };\n\n headers.forEach((header, colIndex) => {\n const value = row[colIndex] !== undefined ? String(row[colIndex]).trim() : '';\n obj[header] = value;\n obj[normalizeKey(header)] = value;\n });\n\n return obj;\n});\n\nconst emailRegex = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/i;\n\nconst candidatos = rows\n .map(row => {\n const rowNumber = row.__rowNumber;\n const noCaso = getValue(row, ['NO.', 'NO', 'No']);\n const hoja = getValue(row, ['Hoja']);\n const nombre = getValue(row, ['Nombre']).replace(/\\s+/g, ' ').trim();\n const emailPersonal = getValue(row, ['Email personal', 'Email Personal', 'Correo personal']).toLowerCase();\n const pdfFileId = getValue(row, ['PDF File ID']);\n let pdfUrl = getValue(row, ['PDF URL']);\n const estadoEnvio = getValue(row, ['Estado envío', 'Estado Envio']);\n const observacionActual = getValue(row, ['Observación', 'Observacion']);\n\n if (!pdfUrl && pdfFileId) {\n pdfUrl = `https://drive.google.com/file/d/${pdfFileId}/view?usp=drivesdk`;\n }\n\n return {\n rowNumber,\n noCaso,\n hoja,\n nombre,\n emailPersonal,\n pdfFileId,\n pdfUrl,\n estadoEnvio,\n observacionActual,\n estadoRange: `CONTROL_ENVIOS!${colEstado.letter}${rowNumber}`,\n observacionRange: `CONTROL_ENVIOS!${colObservacion.letter}${rowNumber}`\n };\n })\n .filter(row => normalizeText(row.estadoEnvio) === 'LISTO_PARA_ENVIAR');\n\nif (candidatos.length === 0) {\n throw new Error('No hay filas con Estado envío = LISTO_PARA_ENVIAR. No se enviará ningún correo.');\n}\n\nconst errores = [];\nconst seenEmails = new Map();\n\nfor (const row of candidatos) {\n if (!row.nombre) {\n errores.push(`Fila ${row.rowNumber}: falta Nombre.`);\n }\n\n if (!row.emailPersonal) {\n errores.push(`Fila ${row.rowNumber} / ${row.nombre}: falta Email personal.`);\n } else if (!emailRegex.test(row.emailPersonal)) {\n errores.push(`Fila ${row.rowNumber} / ${row.nombre}: email inválido \"${row.emailPersonal}\".`);\n } else if (row.emailPersonal.includes('@gamil.')) {\n errores.push(`Fila ${row.rowNumber} / ${row.nombre}: email sospechoso con dominio gamil.com \"${row.emailPersonal}\".`);\n }\n\n if (!row.pdfFileId) {\n errores.push(`Fila ${row.rowNumber} / ${row.nombre}: falta PDF File ID.`);\n }\n\n if (!row.pdfUrl) {\n errores.push(`Fila ${row.rowNumber} / ${row.nombre}: falta PDF URL y no se pudo construir desde PDF File ID.`);\n }\n\n const key = row.emailPersonal.toLowerCase();\n if (key) {\n if (seenEmails.has(key)) {\n errores.push(`Email duplicado \"${row.emailPersonal}\" en filas ${seenEmails.get(key)} y ${row.rowNumber}. Revisar antes de enviar.`);\n } else {\n seenEmails.set(key, row.rowNumber);\n }\n }\n\n const nombreNormalizado = normalizeText(row.nombre);\n const hojaNormalizada = normalizeText(row.hoja);\n\n if (nombreNormalizado.includes('IRMA ALEJANDRA VENTURA') || hojaNormalizada === 'L29') {\n errores.push(`SEGURIDAD: Irma Alejandra Ventura / L29 aparece como LISTO_PARA_ENVIAR en la fila ${row.rowNumber}. No se permite continuar.`);\n }\n}\n\nif (errores.length > 0) {\n throw new Error('Validación detenida. Corrige estos puntos antes de enviar:\\n\\n' + errores.join('\\n'));\n}\n\nconst logoUrl = 'https://dbit.digitalcompass.agency/storage/v1/object/public/public-assets/GLM_completo.png';\n\nreturn candidatos.map(row => ({\n json: {\n ...row,\n logoUrl,\n subject: `${row.nombre} | Detalles de finiquito`,\n attachmentName: `${row.nombre} - Detalles de finiquito.pdf`\n }\n}));" + }, + "id": "27af60fe-1661-41eb-98c0-470ee0dd82ad", + "name": "Validar y preparar envíos", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 912, + 2000 + ], + "notesInFlow": true, + "notes": "Valida filas LISTO_PARA_ENVIAR, rechaza errores de correo/PDF, excluye Irma/L29 por seguridad y prepara un item por colaborador." + }, + { + "parameters": { + "url": "=https://www.googleapis.com/drive/v3/files/{{$json.pdfFileId}}?alt=media&supportsAllDrives=true", + "authentication": "predefinedCredentialType", + "nodeCredentialType": "googleDriveOAuth2Api", + "options": { + "response": { + "response": { + "responseFormat": "file" + } + } + } + }, + "id": "7c7623b8-01db-4859-a450-09da467176b6", + "name": "Descargar PDF", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 1728, + 2016 + ], + "retryOnFail": true, + "maxTries": 3, + "waitBetweenTries": 5000, + "credentials": { + "googleDriveOAuth2Api": { + "id": "g23xdGLZRzBGqKgH", + "name": "Isaac - Google Drive" + } + } + }, + { + "parameters": { + "jsCode": "const items = $input.all();\n\nfunction escapeHtml(value) {\n return String(value ?? '')\n .replace(/&/g, '&')\n .replace(//g, '>')\n .replace(/\"/g, '"');\n}\n\nreturn items.map(item => {\n const source = item.json;\n const binary = item.binary || {};\n\n const nombre = escapeHtml(source.nombre);\n const pdfUrl = escapeHtml(source.pdfUrl || '#');\n const logoUrl = escapeHtml(source.logoUrl || 'https://dbit.digitalcompass.agency/storage/v1/object/public/public-assets/GLM_completo.png');\n\n const html = `\n\n\n\n \n \n Detalles de finiquito\n\n\n\n\n
\n Tu detalle de finiquito laboral está adjunto a este correo.\n
\n\n \n \n \n \n
\n\n \n\n \n \n \n\n \n \n \n\n \n \n \n\n
\n \"GomezLee\n
 
\n\n

\n Detalles de finiquito\n

\n\n

\n Hola ${nombre},\n

\n\n

\n Adjunto encontrarás el detalle de tu finiquito laboral correspondiente a tu proceso de finalización con GomezLee Marketing.\n

\n\n

\n Te pedimos por favor revisar la información con atención y aprobar por esta vía. Si tienes alguna duda o necesitas validar algún dato, puedes responder a este correo para que el equipo correspondiente pueda darte seguimiento. Luego de tu aprobación te estaremos contactando para el pago.\n

\n\n

\n El documento está adjunto a este correo. También puedes abrirlo desde el siguiente enlace:\n

\n\n \n \n \n \n
\n \n Abrir documento\n \n
\n\n \n \n \n \n
\n Este documento contiene información confidencial. Por favor no lo compartas fuera de los canales correspondientes.\n
\n\n

\n Saludos cordiales,
\n GomezLee Marketing\n

\n\n
\n\n
\n\n\n\n`;\n\n if (binary.data) {\n binary.data.fileName = source.attachmentName || `${source.nombre} - Detalles de finiquito.pdf`;\n binary.data.mimeType = 'application/pdf';\n }\n\n return {\n json: {\n ...source,\n html\n },\n binary\n };\n});" + }, + "id": "2ec1eb01-e956-42a1-a0e8-d52cbc8c109b", + "name": "Construir HTML por empleado", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 2016, + 2016 + ], + "notesInFlow": true, + "notes": "Construye el correo real para cada persona: Hola + nombre correspondiente, logo GLM real, botón y adjunto PDF." + }, + { + "parameters": { + "sendTo": "={{$json[\"emailPersonal\"]}}", + "subject": "={{$json[\"subject\"]}}", + "message": "={{$json[\"html\"]}}", + "options": { + "attachmentsUi": { + "attachmentsBinary": [ + {} + ] + }, + "senderName": "Rocío Galvez", + "replyTo": "rgalvez@gomezleemarketing.com" + } + }, + "id": "9cffb51e-ed58-42ce-8044-e41365053790", + "name": "Enviar correo a colaborador", + "type": "n8n-nodes-base.gmail", + "typeVersion": 2.1, + "position": [ + 2288, + 2016 + ], + "webhookId": "5d0344ac-b2d4-4279-a7d3-e5a3f12a282d", + "retryOnFail": true, + "maxTries": 3, + "waitBetweenTries": 5000, + "credentials": { + "gmailOAuth2": { + "id": "UDcO1FLJqA453V2D", + "name": "Gmail account 3" + } + } + }, + { + "parameters": { + "jsCode": "const enviados = $input.all();\nconst fuentes = $('Construir HTML por empleado').all();\n\nif (enviados.length !== fuentes.length) {\n throw new Error(`La cantidad de correos enviados (${enviados.length}) no coincide con la cantidad preparada (${fuentes.length}).`);\n}\n\nreturn enviados.map((item, index) => {\n const source = fuentes[index].json;\n const fecha = new Date().toISOString();\n\n return {\n json: {\n ...source,\n gmailId: item.json.id || item.json.messageId || '',\n sentAt: fecha,\n observacionEnvio: `Correo enviado por n8n el ${fecha}. Acceso de lectura otorgado al PDF y documento adjunto.`\n }\n };\n});" + }, + "id": "528e4272-351a-4e38-b9e8-704aeba503ad", + "name": "Preparar actualización de CONTROL_ENVIOS", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 3472, + 2000 + ] + }, + { + "parameters": { + "method": "PUT", + "url": "==https://sheets.googleapis.com/v4/spreadsheets/1Y9XhRjSSxtymV7Nm0gYbC5lLuGD2zPTc7IVb8PxoxjY/values/{{$json.estadoRange}}?valueInputOption=USER_ENTERED", + "authentication": "predefinedCredentialType", + "nodeCredentialType": "googleSheetsOAuth2Api", + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={{ { values: [[ \"ENVIADO\" ]] } }}", + "options": {} + }, + "id": "a3f1ea0c-8c3c-456d-ab7a-e1a6d0e10565", + "name": "Marcar ENVIADO", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 3744, + 1920 + ], + "notesInFlow": true, + "retryOnFail": true, + "maxTries": 3, + "waitBetweenTries": 5000, + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + }, + "notes": "Actualiza la columna Estado envío a ENVIADO solo después de que Gmail confirma el envío." + }, + { + "parameters": { + "jsCode": "const fuentes = $('Preparar actualización de CONTROL_ENVIOS').all();\nconst respuestas = $input.all();\n\nif (respuestas.length !== fuentes.length) {\n throw new Error(`La cantidad de actualizaciones de estado (${respuestas.length}) no coincide con la cantidad enviada (${fuentes.length}).`);\n}\n\nreturn respuestas.map((item, index) => ({\n json: fuentes[index].json\n}));" + }, + "id": "df873088-035e-4b3e-9d99-94b0d1d15e6f", + "name": "Restaurar datos tras marcar estado", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 4032, + 2000 + ] + }, + { + "parameters": { + "method": "PUT", + "url": "==https://sheets.googleapis.com/v4/spreadsheets/1Y9XhRjSSxtymV7Nm0gYbC5lLuGD2zPTc7IVb8PxoxjY/values/{{$json.observacionRange}}?valueInputOption=USER_ENTERED", + "authentication": "predefinedCredentialType", + "nodeCredentialType": "googleSheetsOAuth2Api", + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={{ { values: [[ $json.observacionEnvio ]] } }}", + "options": {} + }, + "id": "691abc7d-6ded-49aa-a805-d94efa62703a", + "name": "Actualizar observación", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 4304, + 2000 + ], + "retryOnFail": true, + "maxTries": 3, + "waitBetweenTries": 5000, + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + } + }, + { + "parameters": { + "jsCode": "const fuentes = $('Preparar actualización de CONTROL_ENVIOS').all();\n\nreturn [\n {\n json: {\n estado: 'ENVIO_MASIVO_COMPLETADO',\n totalCorreosEnviados: fuentes.length,\n enviados: fuentes.map(item => ({\n nombre: item.json.nombre,\n email: item.json.emailPersonal,\n hoja: item.json.hoja,\n pdfFileId: item.json.pdfFileId,\n gmailId: item.json.gmailId || ''\n })),\n nota: 'Se otorgó acceso de lectura al PDF antes de cada envío. Se actualizó CONTROL_ENVIOS a ENVIADO para los registros procesados.',\n fecha: new Date().toISOString()\n }\n }\n];" + }, + "id": "ea9a0369-8b23-4816-a1f0-f41f2206fbcb", + "name": "Resumen final", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 4592, + 2000 + ] + }, + { + "parameters": { + "content": "## 1) Lectura y validación\n\nLee CONTROL_ENVIOS y prepara solo las filas con:\n\n**Estado envío = LISTO_PARA_ENVIAR**\n\nAquí se validan los datos obligatorios antes de continuar:\n\n- Nombre del colaborador\n- Email personal\n- PDF File ID\n- PDF URL\n- Estado de envío\n\nSi alguna fila tiene datos incompletos o inválidos, el flujo se detiene para evitar errores en el envío.", + "height": 464, + "width": 784 + }, + "type": "n8n-nodes-base.stickyNote", + "typeVersion": 1, + "position": [ + 304, + 1712 + ], + "id": "505b4875-c922-4b2b-8caf-5a2b543ee660", + "name": "Sticky Note" + }, + { + "parameters": { + "content": "## 2) Acceso al PDF + correo personalizado\n\nPor cada colaborador, el flujo primero otorga acceso de lectura al PDF correspondiente.\n\nLuego descarga el PDF y construye el correo HTML personalizado con:\n\n**Hola [Nombre del colaborador],**\n\nCada persona recibe únicamente su propio documento adjunto y su propio enlace de Drive.", + "height": 384, + "width": 1312, + "color": 5 + }, + "type": "n8n-nodes-base.stickyNote", + "typeVersion": 1, + "position": [ + 1440, + 1824 + ], + "id": "23630222-28f7-4675-b6f5-0fe0c5951e5c", + "name": "Sticky Note1" + }, + { + "parameters": { + "content": "## 3) Registro posterior al envío\n\nDespués de enviar el correo correctamente, el flujo actualiza CONTROL_ENVIOS.\n\nMarca la fila como:\n\n**ENVIADO**\n\nTambién actualiza la observación para dejar trazabilidad del envío.\n\nEjecutar este workflow únicamente cuando Administración / Nómina den el visto bueno final.", + "height": 528, + "width": 1520, + "color": 4 + }, + "type": "n8n-nodes-base.stickyNote", + "typeVersion": 1, + "position": [ + 3280, + 1696 + ], + "id": "9386eef4-0dd9-4651-ac46-550be3c703e7", + "name": "Sticky Note2" + } + ], + "connections": { + "Manual Trigger": { + "main": [ + [ + { + "node": "Leer CONTROL_ENVIOS", + "type": "main", + "index": 0 + } + ] + ] + }, + "Leer CONTROL_ENVIOS": { + "main": [ + [ + { + "node": "Validar y preparar envíos", + "type": "main", + "index": 0 + } + ] + ] + }, + "Validar y preparar envíos": { + "main": [ + [ + { + "node": "Descargar PDF", + "type": "main", + "index": 0 + } + ] + ] + }, + "Descargar PDF": { + "main": [ + [ + { + "node": "Construir HTML por empleado", + "type": "main", + "index": 0 + } + ] + ] + }, + "Construir HTML por empleado": { + "main": [ + [ + { + "node": "Enviar correo a colaborador", + "type": "main", + "index": 0 + } + ] + ] + }, + "Enviar correo a colaborador": { + "main": [ + [ + { + "node": "Preparar actualización de CONTROL_ENVIOS", + "type": "main", + "index": 0 + } + ] + ] + }, + "Preparar actualización de CONTROL_ENVIOS": { + "main": [ + [ + { + "node": "Marcar ENVIADO", + "type": "main", + "index": 0 + } + ] + ] + }, + "Marcar ENVIADO": { + "main": [ + [ + { + "node": "Restaurar datos tras marcar estado", + "type": "main", + "index": 0 + } + ] + ] + }, + "Restaurar datos tras marcar estado": { + "main": [ + [ + { + "node": "Actualizar observación", + "type": "main", + "index": 0 + } + ] + ] + }, + "Actualizar observación": { + "main": [ + [ + { + "node": "Resumen final", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "pinData": {}, + "meta": { + "templateCredsSetupCompleted": true, + "instanceId": "b4b77b17af092830e794eef639ce2f6d7daccf7eddc075060b03b3b6545aac70" + } +} \ No newline at end of file