From 50ecdb3d0b6cdc6114226c9884bd763a68ee798a Mon Sep 17 00:00:00 2001 From: Isaac_Aracena Date: Sun, 2 Aug 2026 04:00:35 +0000 Subject: [PATCH] =?UTF-8?q?Update=20backup:=20Portal=20de=20Verificaci?= =?UTF-8?q?=C3=B3n=20de=20N=C3=B3mina=20-=20TT?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- portal-de-verificacion-de-nomina-tt.json | 474 +++++++++++------------ 1 file changed, 229 insertions(+), 245 deletions(-) diff --git a/portal-de-verificacion-de-nomina-tt.json b/portal-de-verificacion-de-nomina-tt.json index 1398be4..5e53b22 100644 --- a/portal-de-verificacion-de-nomina-tt.json +++ b/portal-de-verificacion-de-nomina-tt.json @@ -1,5 +1,5 @@ { - "updatedAt": "2026-07-25T17:28:34.566Z", + "updatedAt": "2026-07-27T15:01:25.665Z", "createdAt": "2026-07-14T14:51:32.549Z", "id": "5AujMxduslftVg9z", "name": "Portal de Verificación de Nómina - TT", @@ -17,10 +17,10 @@ "type": "n8n-nodes-base.webhook", "typeVersion": 2.1, "position": [ - 3200, - 6416 + 2848, + 7344 ], - "id": "74d32f2c-d24e-402c-9af8-5aceb5664112", + "id": "0a3764cc-5cc2-4c62-93f8-1d201ec45e9d", "name": "Webhook", "webhookId": "9c730860-7790-43a5-a3c0-bf5984ced244" }, @@ -31,10 +31,10 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 3440, - 6416 + 3088, + 7344 ], - "id": "79c8b752-1397-4235-8b22-902c35feb488", + "id": "bdfa28aa-5a9e-4c08-9869-0bafa2a8cb52", "name": "Preparar entrada app" }, { @@ -44,10 +44,10 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 3776, - 6112 + 3424, + 7040 ], - "id": "8b00e487-ac48-4d19-a66e-4a722d57ba58", + "id": "46e46f3c-85f9-40ef-a3cc-ee20acc46d73", "name": "Parsear CSV banco TT" }, { @@ -62,10 +62,10 @@ "type": "n8n-nodes-base.extractFromFile", "typeVersion": 1.1, "position": [ - 3776, - 6512 + 3424, + 7440 ], - "id": "be8b25e6-db83-4b01-8d3e-1a0635481833", + "id": "444192a4-bd82-4086-a87f-ab116517f723", "name": "Extract - BICE", "retryOnFail": false }, @@ -81,10 +81,10 @@ "type": "n8n-nodes-base.extractFromFile", "typeVersion": 1.1, "position": [ - 3776, - 6688 + 3424, + 7616 ], - "id": "9bda9647-b34e-4cba-9ec5-88b3e758e084", + "id": "15b0af6e-5e38-4c0c-9d30-496ad9df9413", "name": "Extract - Goldey Samuel", "retryOnFail": false }, @@ -100,10 +100,10 @@ "type": "n8n-nodes-base.extractFromFile", "typeVersion": 1.1, "position": [ - 3776, - 6848 + 3424, + 7776 ], - "id": "3fdc27b6-e651-4a3c-9654-d554e1affd7a", + "id": "22845609-e9b5-486b-88ce-bc5d73b96a2e", "name": "Extract - P&G", "retryOnFail": false }, @@ -119,10 +119,10 @@ "type": "n8n-nodes-base.extractFromFile", "typeVersion": 1.1, "position": [ - 3776, - 7024 + 3424, + 7952 ], - "id": "4ed66d6e-9ca0-430a-8361-71ee2f7bbf98", + "id": "9a39edbc-1e2e-4d75-83ed-9ce48c808abf", "name": "Extract - Whirlpool", "retryOnFail": false }, @@ -138,10 +138,10 @@ "type": "n8n-nodes-base.extractFromFile", "typeVersion": 1.1, "position": [ - 3776, - 7200 + 3424, + 8128 ], - "id": "da474228-1954-4ea2-9ce6-b1880754dca0", + "id": "1710469a-3b4a-4a3b-9c61-77d7d3c4fffb", "name": "Extract - KAD", "retryOnFail": false }, @@ -157,10 +157,10 @@ "type": "n8n-nodes-base.extractFromFile", "typeVersion": 1.1, "position": [ - 3776, - 7360 + 3424, + 8288 ], - "id": "34fb614f-9c2e-4d29-9296-7ac76b21c097", + "id": "72c876ce-f1e4-4f82-ac7d-7d409eb18e64", "name": "Extract - GLM People", "retryOnFail": false }, @@ -176,10 +176,10 @@ "type": "n8n-nodes-base.extractFromFile", "typeVersion": 1.1, "position": [ - 3776, - 7536 + 3424, + 8464 ], - "id": "25847676-a237-4471-931a-c71f35b690b7", + "id": "26d185f0-3f60-44cd-b20e-1fbfbae48fc8", "name": "Extract - GLM", "retryOnFail": false }, @@ -188,10 +188,10 @@ "type": "n8n-nodes-base.merge", "typeVersion": 3.2, "position": [ - 5440, - 6608 + 5088, + 7536 ], - "id": "f2ad5ad8-120f-40fe-b078-97b979d7f2e1", + "id": "b98e1002-cf47-4cde-94d7-08777b928d36", "name": "Merge Hojas TT 01-02" }, { @@ -199,10 +199,10 @@ "type": "n8n-nodes-base.merge", "typeVersion": 3.2, "position": [ - 5440, - 6768 + 5088, + 7696 ], - "id": "6d358ee2-9317-40d1-8171-5cb536736300", + "id": "c7c11485-0d25-4959-ba19-cf631591472a", "name": "Merge Hojas TT 03" }, { @@ -210,10 +210,10 @@ "type": "n8n-nodes-base.merge", "typeVersion": 3.2, "position": [ - 5440, - 6944 + 5088, + 7872 ], - "id": "8da68511-831e-47f3-a5d6-6d25183caac1", + "id": "706b5c48-c1f0-4a12-be90-51abd68f32f3", "name": "Merge Hojas TT 04" }, { @@ -221,10 +221,10 @@ "type": "n8n-nodes-base.merge", "typeVersion": 3.2, "position": [ - 5440, - 7104 + 5088, + 8032 ], - "id": "e8527452-f631-44e7-b37a-5be8bdb0f443", + "id": "a6cff463-cf7d-4e70-b13d-213dbcefa388", "name": "Merge Hojas TT 05" }, { @@ -232,10 +232,10 @@ "type": "n8n-nodes-base.merge", "typeVersion": 3.2, "position": [ - 5440, - 7280 + 5088, + 8208 ], - "id": "7b96730c-826c-4076-993b-5b55a2396f49", + "id": "25f84f87-c966-4b81-ac10-9021b7eeadc3", "name": "Merge Hojas TT 06" }, { @@ -243,10 +243,10 @@ "type": "n8n-nodes-base.merge", "typeVersion": 3.2, "position": [ - 5440, - 7456 + 5088, + 8384 ], - "id": "17bec528-8fcd-41e1-a485-00ea6d1f7e5f", + "id": "75156223-c7d3-4b98-bdc5-aa62a9185db1", "name": "Merge Hojas TT 07" }, { @@ -256,10 +256,10 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 5808, - 7024 + 5456, + 7952 ], - "id": "e960c9ca-88dd-4388-8da2-b8575360ed44", + "id": "d036e80f-bf9c-4024-9bdb-f9222d8ee057", "name": "Normalizar Nómina TT" }, { @@ -271,10 +271,10 @@ "type": "n8n-nodes-base.merge", "typeVersion": 3.2, "position": [ - 6064, - 6368 + 5712, + 7296 ], - "id": "f053d7f4-f031-4379-8999-48c47ebcec37", + "id": "8d2116b6-d720-47a0-b532-aec6f16966c1", "name": "Merge Banco + Nómina TT" }, { @@ -323,10 +323,10 @@ "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.4, "position": [ - 3776, - 5888 + 3424, + 6816 ], - "id": "ddb78ab5-fdf3-45a0-b069-01adead3ad87", + "id": "84b74d83-a969-4f62-a11b-ace145d64e8c", "name": "HTTP - Empleados BambooHR TT", "retryOnFail": true, "maxTries": 3, @@ -345,10 +345,10 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 5440, - 5888 + 5088, + 6816 ], - "id": "713fdd46-81bb-4a0b-b75c-47ef54371a1b", + "id": "d6d947b2-4a0e-4917-855c-7b69a27dae4e", "name": "Normalizar BambooHR TT" }, { @@ -360,10 +360,10 @@ "type": "n8n-nodes-base.merge", "typeVersion": 3.2, "position": [ - 6320, - 6368 + 5968, + 7296 ], - "id": "2b803f96-0829-430e-bef8-4db170337ad4", + "id": "9760d7dd-776e-4ae7-b56b-2f8ac85cc768", "name": "Merge - Agregar BambooHR TT" }, { @@ -373,23 +373,23 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 6576, - 6368 + 6224, + 7296 ], - "id": "53f3881a-3cf3-44a3-b68c-619aae022b1d", + "id": "ea4d4d89-a2f9-4c3f-9c53-803706173b27", "name": "Cruzar Nómina vs Banco" }, { "parameters": { - "jsCode": "const data = $input.first().json || {};\n\nfunction normalizeText(value) {\n return String(value ?? '')\n .replace(/\\uFEFF/g, '')\n .replace(/\\u00A0/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim();\n}\n\nfunction roundMoney(value) {\n return Math.round(\n (Number(value) || 0) * 100\n ) / 100;\n}\n\nfunction firstValue(value) {\n if (Array.isArray(value)) {\n return value\n .map(normalizeText)\n .filter(Boolean)\n .join(' / ');\n }\n\n return normalizeText(value);\n}\n\nfunction formatPeriodEnd(value) {\n const raw = normalizeText(value);\n\n if (!/^\\d{4}-\\d{2}-\\d{2}$/.test(raw)) {\n return raw;\n }\n\n const [year, month, day] = raw.split('-');\n\n const monthNames = {\n '01': 'ene',\n '02': 'feb',\n '03': 'mar',\n '04': 'abr',\n '05': 'may',\n '06': 'jun',\n '07': 'jul',\n '08': 'ago',\n '09': 'sep',\n '10': 'oct',\n '11': 'nov',\n '12': 'dic',\n };\n\n return `${day}-${monthNames[month] || month}-${year}`;\n}\n\nfunction mainReportSense(row, difference) {\n const subcategory = normalizeText(\n row.subcategory\n ).toLowerCase();\n\n const payrollAmount = Number(\n row.payroll_amount ??\n row.payrollAmount ??\n 0\n );\n\n const bankAmount = Number(\n row.bank_amount ??\n row.bankAmount ??\n 0\n );\n\n if (\n subcategory ===\n 'nomina_con_cuenta_sin_pago_banco' ||\n (bankAmount === 0 && payrollAmount > 0)\n ) {\n return 'No aparece pagado en banco';\n }\n\n if (difference > 0) {\n return 'Se pagó de menos';\n }\n\n if (difference < 0) {\n return 'Se pagó de más';\n }\n\n return 'Revisar';\n}\n\nfunction accountValues(value) {\n const values = Array.isArray(value)\n ? value\n : String(value ?? '')\n .split(/\\s*(?:\\/|;|,|\\by\\b)\\s*/i);\n\n return values\n .map((item) =>\n String(item ?? '')\n .replace(/\\u00A0/g, '')\n .replace(/\\.0$/g, '')\n .replace(/\\D/g, '')\n .trim()\n )\n .filter(\n (account) =>\n account.length >= 6 &&\n !/^0+$/.test(account)\n );\n}\n\nfunction payrollAccountsForWrongAccount(row) {\n const candidates = [\n row.payroll_account,\n row.payrollAccount,\n ...(Array.isArray(row.source_rows)\n ? row.source_rows.flatMap(\n (sourceRow) => [\n sourceRow.account,\n sourceRow.payroll_account,\n sourceRow.payrollAccount,\n ]\n )\n : []),\n ];\n\n return Array.from(\n new Set(\n candidates.flatMap(accountValues)\n )\n );\n}\n\nfunction bankAccountForWrongAccount(row) {\n return firstValue(\n row.bank_account ||\n row.bankAccount ||\n row.account ||\n ''\n );\n}\n\nfunction moneyLabel(value) {\n return Math.abs(\n roundMoney(value)\n ).toLocaleString('en-US', {\n minimumFractionDigits: 2,\n maximumFractionDigits: 2,\n });\n}\n\nfunction wrongAccountTotalStatus(row) {\n const payrollAmount = roundMoney(\n row.payroll_amount ??\n row.payrollAmount ??\n 0\n );\n\n const bankAmount = roundMoney(\n row.bank_amount ??\n row.bankAmount ??\n 0\n );\n\n const difference = roundMoney(\n payrollAmount - bankAmount\n );\n\n if (Math.abs(difference) <= 0.02) {\n return (\n 'El total de nómina coincide con ' +\n 'el total pagado por el banco.'\n );\n }\n\n if (difference > 0) {\n return (\n 'El total de nómina supera el total ' +\n `del banco por TT$${moneyLabel(difference)}.`\n );\n }\n\n return (\n 'El total pagado por el banco supera ' +\n `el total de nómina por TT$${moneyLabel(difference)}.`\n );\n}\n\nfunction wrongAccountFinding(row) {\n const existing = normalizeText(\n row.observation || ''\n );\n\n if (existing) return existing;\n\n const payrollAccounts =\n payrollAccountsForWrongAccount(row);\n\n const bankAccount =\n bankAccountForWrongAccount(row);\n\n return (\n 'El empleado presenta una posible ' +\n 'inconsistencia entre la cuenta registrada ' +\n `en nómina (${payrollAccounts.join(' y ') || 'sin cuenta identificada'}) ` +\n `y la cuenta utilizada por el banco (${bankAccount || 'sin cuenta identificada'}).`\n );\n}\n\nconst metadata = data.metadata || {};\nconst summary = data.summary || {};\n\nconst rows = Array.isArray(data.rows)\n ? data.rows\n : [];\n\nconst bankWithoutBamboo =\n Array.isArray(data.bankWithoutBamboo)\n ? data.bankWithoutBamboo\n : [];\n\nconst periodLabel =\n metadata.period_label ||\n `${metadata.year || ''}-${metadata.month || ''}-${metadata.period_type || ''}`;\n\nconst periodEndLabel = formatPeriodEnd(\n metadata.period_end || ''\n);\n\nconst spreadsheetTitle =\n `Cruce de Cuentas GLM TT - ${periodLabel}`;\n\nconst sheetIds = {\n nominaVsBanco: 201,\n bancoSinNomina: 202,\n bancoSinBamboo: 203,\n diferenciasNombreBanco: 204,\n cuentaMalDigitada: 205,\n resumen: 206,\n};\n\nconst cuentaMalDigitadaCases = rows.filter(\n (row) =>\n row.category ===\n 'posible_cuenta_mal_digitada'\n);\n\nconst hasCuentaMalDigitada =\n cuentaMalDigitadaCases.length > 0;\n\nconst sheetTitles = {\n nominaVsBanco:\n '01 Nómina vs Banco',\n bancoSinNomina:\n '02 Banco sin Nómina',\n bancoSinBamboo:\n '03 Banco sin Bamboo',\n diferenciasNombreBanco:\n '04 Diferencias de Nombre',\n cuentaMalDigitada:\n '05 Cuenta Mal Digitada',\n resumen: hasCuentaMalDigitada\n ? '06 Resumen'\n : '05 Resumen',\n};\n\nconst mainRows = rows\n .filter(\n (row) =>\n row.category === 'discrepancia'\n )\n .map((row, index) => {\n const payrollAmount = roundMoney(\n row.payroll_amount ??\n row.payrollAmount ??\n 0\n );\n\n const bankAmount = roundMoney(\n row.bank_amount ??\n row.bankAmount ??\n 0\n );\n\n const difference = roundMoney(\n row.difference ??\n (payrollAmount - bankAmount)\n );\n\n return [\n index + 1,\n normalizeText(\n row.employee_name ||\n row.employee ||\n ''\n ),\n normalizeText(\n row.bank_account ||\n row.bankAccount ||\n row.payroll_account ||\n row.payrollAccount ||\n row.account ||\n ''\n ),\n payrollAmount,\n bankAmount,\n difference,\n mainReportSense(\n row,\n difference\n ),\n normalizeText(\n row.status || 'Riesgo'\n ).toUpperCase(),\n '',\n ];\n });\n\nconst bancoSinNominaRows = rows\n .filter(\n (row) =>\n row.category ===\n 'banco_sin_nomina'\n )\n .map((row, index) => [\n index + 1,\n normalizeText(\n row.employee_name ||\n row.employee ||\n ''\n ),\n normalizeText(\n row.bank_account ||\n row.bankAccount ||\n row.account ||\n ''\n ),\n roundMoney(\n row.bank_amount ??\n row.bankAmount ??\n 0\n ),\n firstValue(\n row.source_files ||\n row.source_file ||\n ''\n ),\n normalizeText(\n row.status ||\n 'Pendiente revisión'\n ).toUpperCase(),\n normalizeText(\n row.observation || ''\n ),\n '',\n ]);\n\nconst bancoSinBambooRows =\n bankWithoutBamboo.map(\n (row, index) => [\n index + 1,\n normalizeText(\n row.bank_name_file ||\n row.employee_name ||\n row.employee ||\n ''\n ),\n normalizeText(\n row.bank_account ||\n row.bankAccount ||\n row.account ||\n ''\n ),\n roundMoney(\n row.bank_amount ??\n row.bankAmount ??\n 0\n ),\n firstValue(\n row.source_files ||\n row.source_file ||\n ''\n ),\n 'PENDIENTE REVISIÓN',\n '',\n ]\n );\n\nconst diferenciasNombreRows = rows\n .filter(\n (row) =>\n row.category ===\n 'diferencia_nombre_banco'\n )\n .map((row, index) => [\n index + 1,\n normalizeText(\n row.payroll_name ||\n row.employee_name ||\n row.employee ||\n row.bank_name_file ||\n ''\n ),\n normalizeText(\n row.bank_name ||\n row.bank_account_holder ||\n ''\n ),\n normalizeText(\n row.bank_account ||\n row.bankAccount ||\n row.account ||\n ''\n ),\n roundMoney(\n row.bank_amount ??\n row.bankAmount ??\n 0\n ),\n normalizeText(\n row.status ||\n 'Pendiente revisión'\n ).toUpperCase(),\n normalizeText(\n row.observation || ''\n ),\n '',\n ]);\n\nconst cuentaMalDigitadaRows = [];\n\ncuentaMalDigitadaCases.forEach(\n (row, index) => {\n const payrollAccounts =\n payrollAccountsForWrongAccount(row);\n\n const bankAccount =\n bankAccountForWrongAccount(row);\n\n const fields = [\n [\n 'Empleado',\n normalizeText(\n row.employee_name ||\n row.employee ||\n ''\n ),\n ],\n [\n 'Cuentas registradas en las hojas de nómina',\n payrollAccounts.join(' y ') ||\n 'No se identificó una cuenta válida en la nómina.',\n ],\n [\n 'Cuenta utilizada por el banco',\n bankAccount ||\n 'No se identificó una cuenta válida en el banco.',\n ],\n [\n 'Estado del total',\n wrongAccountTotalStatus(row),\n ],\n [\n 'Hallazgo',\n wrongAccountFinding(row),\n ],\n [\n 'Clasificación',\n 'Posible cuenta mal digitada — revisar y unificar la cuenta registrada en nómina.',\n ],\n ];\n\n fields.forEach(\n (field, fieldIndex) => {\n cuentaMalDigitadaRows.push([\n fieldIndex === 0\n ? index + 1\n : '',\n field[0],\n field[1],\n '',\n ]);\n }\n );\n }\n);\n\nconst resumenRows = [\n ['Período', periodLabel],\n [\n 'Coincidencias',\n Number(summary.coincidencias || 0),\n ],\n [\n 'Discrepancias de monto o pago',\n Number(\n summary.discrepanciasMontoPago ??\n summary.discrepancias ??\n 0\n ),\n ],\n [\n 'Banco sin nómina',\n Number(summary.bancoSinNomina || 0),\n ],\n [\n 'Banco sin Bamboo',\n Number(summary.bancoSinBamboo || 0),\n ],\n [\n 'Nómina sin cuenta no conciliada',\n Number(summary.nominaSinCuenta || 0),\n ],\n [\n 'Diferencias de nombre',\n Number(\n summary.diferenciasNombreBanco || 0\n ),\n ],\n [\n 'Posibles cuentas mal digitadas',\n Number(\n summary.posiblesCuentasMalDigitadas || 0\n ),\n ],\n [\n 'Pendientes del cruce principal',\n Number(summary.pendientes || 0),\n ],\n [\n 'Empleados BambooHR Trinidad y Tobago',\n Number(summary.empleadosBambooTT || 0),\n ],\n [\n 'Empleados BambooHR en el período',\n Number(\n summary.empleadosBambooEnPeriodo || 0\n ),\n ],\n [\n 'Filas válidas de nómina',\n Number(\n summary.filasNominaValidas || 0\n ),\n ],\n [\n 'Filas de nómina sin cuenta detectadas',\n Number(\n summary.filasNominaSinCuenta || 0\n ),\n ],\n [\n 'Transacciones bancarias',\n Number(\n summary.transaccionesBanco || 0\n ),\n ],\n [\n 'Total nómina',\n roundMoney(summary.totalNomina || 0),\n ],\n [\n 'Total banco',\n roundMoney(summary.totalBanco || 0),\n ],\n [\n 'Diferencia total',\n roundMoney(\n summary.diferenciaTotal || 0\n ),\n ],\n];\n\nfunction reportValues(\n title,\n subtitle,\n header,\n body\n) {\n return [\n [\n title,\n ...Array(\n Math.max(header.length - 1, 0)\n ).fill(''),\n ],\n [\n subtitle,\n ...Array(\n Math.max(header.length - 1, 0)\n ).fill(''),\n ],\n Array(header.length).fill(''),\n header,\n ...body,\n ];\n}\n\nconst nominaVsBancoValues =\n reportValues(\n 'GOMEZLEE MARKETING',\n `Diferencias de Monto Nómina vs. Banco · Trinidad y Tobago · ${periodEndLabel}`,\n [\n '#',\n 'Empleado',\n 'Cuenta',\n 'Monto en Nómina (TT$)',\n 'Monto en Banco (TT$)',\n 'Diferencia (TT$)',\n 'Sentido',\n 'Estado',\n 'Resolución',\n ],\n mainRows\n );\n\nconst bancoSinNominaValues =\n reportValues(\n 'GOMEZLEE MARKETING',\n `Pagos bancarios sin registro en la nómina · Trinidad y Tobago · ${periodEndLabel}`,\n [\n '#',\n 'Nombre en banco',\n 'Cuenta',\n 'Monto en banco (TT$)',\n 'Archivo',\n 'Estado',\n 'Observación',\n 'Resolución',\n ],\n bancoSinNominaRows\n );\n\nconst bancoSinBambooValues =\n reportValues(\n 'GOMEZLEE MARKETING',\n `Pagos en banco sin empleado identificado en BambooHR · Trinidad y Tobago · ${periodEndLabel}`,\n [\n '#',\n 'Nombre en banco',\n 'Cuenta',\n 'Monto en banco (TT$)',\n 'Archivo',\n 'Estado',\n 'Resolución',\n ],\n bancoSinBambooRows\n );\n\nconst diferenciasNombreValues =\n reportValues(\n 'GOMEZLEE MARKETING',\n `Diferencias de nombre entre nómina y banco · Trinidad y Tobago · ${periodEndLabel}`,\n [\n '#',\n 'Nombre en nómina',\n 'Nombre en banco',\n 'Cuenta',\n 'Monto en banco (TT$)',\n 'Estado',\n 'Observación',\n 'Resolución',\n ],\n diferenciasNombreRows\n );\n\nconst cuentaMalDigitadaValues =\n reportValues(\n 'GOMEZLEE MARKETING',\n `Cuenta Mal Digitada en Nómina · Trinidad y Tobago · ${periodEndLabel}`,\n ['#', 'Campo', 'Detalle', 'Resolución'],\n cuentaMalDigitadaRows\n );\n\nconst resumenValues =\n reportValues(\n 'GOMEZLEE MARKETING',\n `Resumen del cruce Nómina vs. Banco · Trinidad y Tobago · ${periodEndLabel}`,\n ['Indicador', 'Valor'],\n resumenRows\n );\n\nconst valueData = [\n {\n range:\n `'${sheetTitles.nominaVsBanco}'!A1:I`,\n values: nominaVsBancoValues,\n },\n {\n range:\n `'${sheetTitles.bancoSinNomina}'!A1:H`,\n values: bancoSinNominaValues,\n },\n {\n range:\n `'${sheetTitles.bancoSinBamboo}'!A1:G`,\n values: bancoSinBambooValues,\n },\n {\n range:\n `'${sheetTitles.diferenciasNombreBanco}'!A1:H`,\n values: diferenciasNombreValues,\n },\n ...(hasCuentaMalDigitada\n ? [\n {\n range:\n `'${sheetTitles.cuentaMalDigitada}'!A1:D`,\n values:\n cuentaMalDigitadaValues,\n },\n ]\n : []),\n {\n range:\n `'${sheetTitles.resumen}'!A1:B`,\n values: resumenValues,\n },\n];\n\nconst brandColor = {\n red: 0.29,\n green: 0.49,\n blue: 0.58,\n};\n\nconst whiteColor = {\n red: 1,\n green: 1,\n blue: 1,\n};\n\nconst borderColor = {\n red: 0.82,\n green: 0.86,\n blue: 0.88,\n};\n\nfunction mergeRow(\n sheetId,\n rowIndex,\n columnCount\n) {\n return {\n mergeCells: {\n range: {\n sheetId,\n startRowIndex: rowIndex,\n endRowIndex: rowIndex + 1,\n startColumnIndex: 0,\n endColumnIndex: columnCount,\n },\n mergeType: 'MERGE_ALL',\n },\n };\n}\n\nfunction formatRange(\n sheetId,\n startRowIndex,\n endRowIndex,\n startColumnIndex,\n endColumnIndex,\n userEnteredFormat\n) {\n const formatFields =\n Object.keys(userEnteredFormat || {});\n\n return {\n repeatCell: {\n range: {\n sheetId,\n startRowIndex,\n endRowIndex,\n startColumnIndex,\n endColumnIndex,\n },\n cell: {\n userEnteredFormat,\n },\n fields:\n `userEnteredFormat(${formatFields.join(',')})`,\n },\n };\n}\n\nfunction titleFormat(\n sheetId,\n rowIndex,\n columnCount,\n options = {}\n) {\n return formatRange(\n sheetId,\n rowIndex,\n rowIndex + 1,\n 0,\n columnCount,\n {\n backgroundColor: brandColor,\n textFormat: {\n bold: options.bold ?? true,\n italic:\n options.italic ?? false,\n fontSize:\n options.fontSize ?? 12,\n foregroundColor:\n whiteColor,\n },\n horizontalAlignment: 'LEFT',\n verticalAlignment: 'MIDDLE',\n wrapStrategy: 'WRAP',\n }\n );\n}\n\nfunction headerFormat(\n sheetId,\n columnCount\n) {\n return formatRange(\n sheetId,\n 3,\n 4,\n 0,\n columnCount,\n {\n backgroundColor: brandColor,\n textFormat: {\n bold: true,\n foregroundColor:\n whiteColor,\n },\n horizontalAlignment: 'CENTER',\n verticalAlignment: 'MIDDLE',\n wrapStrategy: 'WRAP',\n }\n );\n}\n\nfunction freezeRows(\n sheetId,\n count\n) {\n return {\n updateSheetProperties: {\n properties: {\n sheetId,\n gridProperties: {\n frozenRowCount: count,\n },\n },\n fields:\n 'gridProperties.frozenRowCount',\n },\n };\n}\n\nfunction setFilter(\n sheetId,\n columnCount,\n endRowIndex\n) {\n return {\n setBasicFilter: {\n filter: {\n range: {\n sheetId,\n startRowIndex: 3,\n endRowIndex,\n startColumnIndex: 0,\n endColumnIndex:\n columnCount,\n },\n },\n },\n };\n}\n\nfunction setColumnWidth(\n sheetId,\n index,\n pixelSize\n) {\n return {\n updateDimensionProperties: {\n range: {\n sheetId,\n dimension: 'COLUMNS',\n startIndex: index,\n endIndex: index + 1,\n },\n properties: {\n pixelSize,\n },\n fields: 'pixelSize',\n },\n };\n}\n\nfunction setRowHeight(\n sheetId,\n startIndex,\n endIndex,\n pixelSize\n) {\n return {\n updateDimensionProperties: {\n range: {\n sheetId,\n dimension: 'ROWS',\n startIndex,\n endIndex,\n },\n properties: {\n pixelSize,\n },\n fields: 'pixelSize',\n },\n };\n}\n\nfunction borderFormat(\n sheetId,\n columnCount,\n endRowIndex\n) {\n const border = {\n style: 'SOLID',\n color: borderColor,\n };\n\n return [\n formatRange(\n sheetId,\n 3,\n endRowIndex,\n 0,\n columnCount,\n {\n verticalAlignment: 'MIDDLE',\n wrapStrategy: 'WRAP',\n }\n ),\n {\n updateBorders: {\n range: {\n sheetId,\n startRowIndex: 3,\n endRowIndex,\n startColumnIndex: 0,\n endColumnIndex: columnCount,\n },\n top: border,\n bottom: border,\n left: border,\n right: border,\n innerHorizontal: border,\n innerVertical: border,\n },\n },\n ];\n}\n\nfunction moneyFormat(\n sheetId,\n startColumnIndex,\n endColumnIndex,\n startRowIndex,\n endRowIndex\n) {\n return formatRange(\n sheetId,\n startRowIndex,\n endRowIndex,\n startColumnIndex,\n endColumnIndex,\n {\n numberFormat: {\n type: 'NUMBER',\n pattern:\n '\"TT$\"#,##0.00',\n },\n horizontalAlignment:\n 'RIGHT',\n verticalAlignment:\n 'MIDDLE',\n }\n );\n}\n\nfunction statusFormat(\n sheetId,\n columnIndex,\n endRowIndex\n) {\n return formatRange(\n sheetId,\n 4,\n endRowIndex,\n columnIndex,\n columnIndex + 1,\n {\n backgroundColor: {\n red: 1,\n green: 0.92,\n blue: 0.92,\n },\n textFormat: {\n bold: true,\n foregroundColor: {\n red: 0.82,\n green: 0.08,\n blue: 0.08,\n },\n },\n horizontalAlignment:\n 'CENTER',\n verticalAlignment:\n 'MIDDLE',\n wrapStrategy: 'WRAP',\n }\n );\n}\n\nfunction conditionalDifference(\n sheetId,\n endRowIndex,\n formula,\n backgroundColor,\n textColor\n) {\n return {\n addConditionalFormatRule: {\n rule: {\n ranges: [\n {\n sheetId,\n startRowIndex: 4,\n endRowIndex,\n startColumnIndex: 5,\n endColumnIndex: 6,\n },\n ],\n booleanRule: {\n condition: {\n type: 'CUSTOM_FORMULA',\n values: [\n {\n userEnteredValue:\n formula,\n },\n ],\n },\n format: {\n backgroundColor,\n textFormat: {\n bold: true,\n foregroundColor:\n textColor,\n },\n },\n },\n },\n index: 0,\n },\n };\n}\n\nfunction styleReport(config) {\n const {\n sheetId,\n columnCount,\n bodyRowsCount,\n widths,\n moneyColumns = [],\n statusColumn = null,\n } = config;\n\n const endRowIndex = Math.max(\n 4 + bodyRowsCount,\n 4\n );\n\n const requests = [\n mergeRow(\n sheetId,\n 0,\n columnCount\n ),\n mergeRow(\n sheetId,\n 1,\n columnCount\n ),\n titleFormat(\n sheetId,\n 0,\n columnCount,\n {\n fontSize: 12,\n bold: true,\n }\n ),\n titleFormat(\n sheetId,\n 1,\n columnCount,\n {\n fontSize: 10,\n bold: false,\n italic: true,\n }\n ),\n headerFormat(\n sheetId,\n columnCount\n ),\n freezeRows(sheetId, 4),\n setFilter(\n sheetId,\n columnCount,\n endRowIndex\n ),\n ...borderFormat(\n sheetId,\n columnCount,\n endRowIndex\n ),\n setRowHeight(\n sheetId,\n 0,\n 1,\n 30\n ),\n setRowHeight(\n sheetId,\n 1,\n 2,\n 28\n ),\n setRowHeight(\n sheetId,\n 3,\n 4,\n 42\n ),\n ...widths.map(\n (width, index) =>\n setColumnWidth(\n sheetId,\n index,\n width\n )\n ),\n ];\n\n if (bodyRowsCount > 0) {\n requests.push(\n setRowHeight(\n sheetId,\n 4,\n endRowIndex,\n 30\n )\n );\n\n for (\n const [startColumn, endColumn] of\n moneyColumns\n ) {\n requests.push(\n moneyFormat(\n sheetId,\n startColumn,\n endColumn,\n 4,\n endRowIndex\n )\n );\n }\n\n if (\n Number.isInteger(\n statusColumn\n )\n ) {\n requests.push(\n statusFormat(\n sheetId,\n statusColumn,\n endRowIndex\n )\n );\n }\n }\n\n return requests;\n}\n\nconst formatRequests = [\n ...styleReport({\n sheetId:\n sheetIds.nominaVsBanco,\n columnCount: 9,\n bodyRowsCount:\n mainRows.length,\n widths: [\n 48,\n 250,\n 145,\n 135,\n 135,\n 135,\n 180,\n 120,\n 260,\n ],\n moneyColumns: [\n [3, 6],\n ],\n statusColumn: 7,\n }),\n\n ...(mainRows.length > 0\n ? [\n conditionalDifference(\n sheetIds.nominaVsBanco,\n 4 + mainRows.length,\n '=$F5>0',\n {\n red: 1,\n green: 0.97,\n blue: 0.82,\n },\n {\n red: 0.45,\n green: 0.27,\n blue: 0,\n }\n ),\n conditionalDifference(\n sheetIds.nominaVsBanco,\n 4 + mainRows.length,\n '=$F5<0',\n {\n red: 1,\n green: 0.89,\n blue: 0.89,\n },\n {\n red: 0.85,\n green: 0.05,\n blue: 0.05,\n }\n ),\n ]\n : []),\n\n ...styleReport({\n sheetId:\n sheetIds.bancoSinNomina,\n columnCount: 8,\n bodyRowsCount:\n bancoSinNominaRows.length,\n widths: [\n 48,\n 230,\n 145,\n 135,\n 230,\n 140,\n 360,\n 260,\n ],\n moneyColumns: [[3, 4]],\n statusColumn: 5,\n }),\n\n ...styleReport({\n sheetId:\n sheetIds.bancoSinBamboo,\n columnCount: 7,\n bodyRowsCount:\n bancoSinBambooRows.length,\n widths: [\n 48,\n 250,\n 145,\n 140,\n 250,\n 150,\n 260,\n ],\n moneyColumns: [[3, 4]],\n statusColumn: 5,\n }),\n\n ...styleReport({\n sheetId:\n sheetIds.diferenciasNombreBanco,\n columnCount: 8,\n bodyRowsCount:\n diferenciasNombreRows.length,\n widths: [\n 48,\n 240,\n 240,\n 145,\n 140,\n 150,\n 420,\n 260,\n ],\n moneyColumns: [[4, 5]],\n statusColumn: 5,\n }),\n];\n\nif (hasCuentaMalDigitada) {\n const endRowIndex =\n 4 +\n cuentaMalDigitadaRows.length;\n\n formatRequests.push(\n ...styleReport({\n sheetId:\n sheetIds.cuentaMalDigitada,\n columnCount: 4,\n bodyRowsCount:\n cuentaMalDigitadaRows.length,\n widths: [\n 48,\n 300,\n 520,\n 260,\n ],\n statusColumn: null,\n })\n );\n\n cuentaMalDigitadaCases.forEach(\n (_, caseIndex) => {\n const startRowIndex =\n 4 + caseIndex * 6;\n\n const endCaseRowIndex =\n startRowIndex + 6;\n\n formatRequests.push(\n {\n mergeCells: {\n range: {\n sheetId:\n sheetIds.cuentaMalDigitada,\n startRowIndex,\n endRowIndex:\n endCaseRowIndex,\n startColumnIndex: 0,\n endColumnIndex: 1,\n },\n mergeType:\n 'MERGE_ALL',\n },\n },\n {\n mergeCells: {\n range: {\n sheetId:\n sheetIds.cuentaMalDigitada,\n startRowIndex,\n endRowIndex:\n endCaseRowIndex,\n startColumnIndex: 3,\n endColumnIndex: 4,\n },\n mergeType:\n 'MERGE_ALL',\n },\n },\n formatRange(\n sheetIds.cuentaMalDigitada,\n startRowIndex,\n endCaseRowIndex,\n 0,\n 1,\n {\n backgroundColor: {\n red: 0.91,\n green: 0.95,\n blue: 0.99,\n },\n textFormat: {\n bold: true,\n foregroundColor: {\n red: 0.20,\n green: 0.36,\n blue: 0.45,\n },\n },\n horizontalAlignment:\n 'CENTER',\n verticalAlignment:\n 'MIDDLE',\n wrapStrategy: 'WRAP',\n }\n ),\n formatRange(\n sheetIds.cuentaMalDigitada,\n startRowIndex,\n endCaseRowIndex,\n 1,\n 2,\n {\n backgroundColor: {\n red: 0.93,\n green: 0.97,\n blue: 0.90,\n },\n textFormat: {\n bold: true,\n foregroundColor: {\n red: 0.20,\n green: 0.36,\n blue: 0.45,\n },\n },\n horizontalAlignment:\n 'LEFT',\n verticalAlignment:\n 'MIDDLE',\n wrapStrategy: 'WRAP',\n }\n ),\n formatRange(\n sheetIds.cuentaMalDigitada,\n startRowIndex,\n endCaseRowIndex,\n 2,\n 3,\n {\n horizontalAlignment:\n 'LEFT',\n verticalAlignment:\n 'MIDDLE',\n wrapStrategy: 'WRAP',\n }\n ),\n setRowHeight(\n sheetIds.cuentaMalDigitada,\n startRowIndex,\n endCaseRowIndex,\n 52\n )\n );\n }\n );\n}\n\nconst resumenEndRowIndex =\n 4 + resumenRows.length;\n\nformatRequests.push(\n ...styleReport({\n sheetId:\n sheetIds.resumen,\n columnCount: 2,\n bodyRowsCount:\n resumenRows.length,\n widths: [340, 180],\n statusColumn: null,\n }),\n moneyFormat(\n sheetIds.resumen,\n 1,\n 2,\n resumenEndRowIndex - 3,\n resumenEndRowIndex\n )\n);\n\nreturn [\n {\n json: {\n ok: true,\n stage:\n 'preparar_google_sheet_tt',\n metadata,\n summary,\n spreadsheetTitle,\n sheetIds,\n sheetTitles,\n createSpreadsheetBody: {\n properties: {\n title: spreadsheetTitle,\n },\n sheets: [\n {\n properties: {\n sheetId:\n sheetIds.nominaVsBanco,\n title:\n sheetTitles.nominaVsBanco,\n },\n },\n {\n properties: {\n sheetId:\n sheetIds.bancoSinNomina,\n title:\n sheetTitles.bancoSinNomina,\n },\n },\n {\n properties: {\n sheetId:\n sheetIds.bancoSinBamboo,\n title:\n sheetTitles.bancoSinBamboo,\n },\n },\n {\n properties: {\n sheetId:\n sheetIds.diferenciasNombreBanco,\n title:\n sheetTitles.diferenciasNombreBanco,\n },\n },\n ...(hasCuentaMalDigitada\n ? [\n {\n properties: {\n sheetId:\n sheetIds.cuentaMalDigitada,\n title:\n sheetTitles.cuentaMalDigitada,\n },\n },\n ]\n : []),\n {\n properties: {\n sheetId:\n sheetIds.resumen,\n title:\n sheetTitles.resumen,\n },\n },\n ],\n },\n valueBatchBody: {\n valueInputOption:\n 'RAW',\n data: valueData,\n },\n formatBatchBody: {\n requests: formatRequests,\n },\n originalResponse: data,\n },\n },\n];" + "jsCode": "const data = $input.first().json || {};\n\nfunction normalizeText(value) {\n return String(value ?? '')\n .replace(/\\uFEFF/g, '')\n .replace(/\\u00A0/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim();\n}\n\nfunction roundMoney(value) {\n return Math.round(\n (Number(value) || 0) * 100\n ) / 100;\n}\n\nfunction firstValue(value) {\n if (Array.isArray(value)) {\n return value\n .map(normalizeText)\n .filter(Boolean)\n .join(' / ');\n }\n\n return normalizeText(value);\n}\n\nfunction formatPeriodEnd(value) {\n const raw = normalizeText(value);\n\n if (!/^\\d{4}-\\d{2}-\\d{2}$/.test(raw)) {\n return raw;\n }\n\n const [year, month, day] = raw.split('-');\n\n const monthNames = {\n '01': 'ene',\n '02': 'feb',\n '03': 'mar',\n '04': 'abr',\n '05': 'may',\n '06': 'jun',\n '07': 'jul',\n '08': 'ago',\n '09': 'sep',\n '10': 'oct',\n '11': 'nov',\n '12': 'dic',\n };\n\n return `${day}-${monthNames[month] || month}-${year}`;\n}\n\nfunction mainReportSense(row, difference) {\n const subcategory = normalizeText(\n row.subcategory\n ).toLowerCase();\n\n const payrollAmount = Number(\n row.payroll_amount ??\n row.payrollAmount ??\n 0\n );\n\n const bankAmount = Number(\n row.bank_amount ??\n row.bankAmount ??\n 0\n );\n\n if (\n subcategory ===\n 'nomina_con_cuenta_sin_pago_banco' ||\n (bankAmount === 0 && payrollAmount > 0)\n ) {\n return 'No aparece pagado en banco';\n }\n\n if (difference > 0) {\n return 'Se pagó de menos';\n }\n\n if (difference < 0) {\n return 'Se pagó de más';\n }\n\n return 'Revisar';\n}\n\nfunction accountValues(value) {\n const values = Array.isArray(value)\n ? value\n : String(value ?? '')\n .split(/\\s*(?:\\/|;|,|\\by\\b)\\s*/i);\n\n return values\n .map((item) =>\n String(item ?? '')\n .replace(/\\u00A0/g, '')\n .replace(/\\.0$/g, '')\n .replace(/\\D/g, '')\n .trim()\n )\n .filter(\n (account) =>\n account.length >= 6 &&\n !/^0+$/.test(account)\n );\n}\n\nfunction payrollAccountsForWrongAccount(row) {\n const candidates = [\n row.payroll_account,\n row.payrollAccount,\n ...(Array.isArray(row.source_rows)\n ? row.source_rows.flatMap(\n (sourceRow) => [\n sourceRow.account,\n sourceRow.payroll_account,\n sourceRow.payrollAccount,\n ]\n )\n : []),\n ];\n\n return Array.from(\n new Set(\n candidates.flatMap(accountValues)\n )\n );\n}\n\nfunction bankAccountForWrongAccount(row) {\n return firstValue(\n row.bank_account ||\n row.bankAccount ||\n row.account ||\n ''\n );\n}\n\nfunction moneyLabel(value) {\n return Math.abs(\n roundMoney(value)\n ).toLocaleString('en-US', {\n minimumFractionDigits: 2,\n maximumFractionDigits: 2,\n });\n}\n\nfunction wrongAccountTotalStatus(row) {\n const payrollAmount = roundMoney(\n row.payroll_amount ??\n row.payrollAmount ??\n 0\n );\n\n const bankAmount = roundMoney(\n row.bank_amount ??\n row.bankAmount ??\n 0\n );\n\n const difference = roundMoney(\n payrollAmount - bankAmount\n );\n\n if (Math.abs(difference) <= 0.02) {\n return (\n 'El total de nómina coincide con ' +\n 'el total pagado por el banco.'\n );\n }\n\n if (difference > 0) {\n return (\n 'El total de nómina supera el total ' +\n `del banco por TT$${moneyLabel(difference)}.`\n );\n }\n\n return (\n 'El total pagado por el banco supera ' +\n `el total de nómina por TT$${moneyLabel(difference)}.`\n );\n}\n\nfunction wrongAccountFinding(row) {\n const existing = normalizeText(\n row.observation || ''\n );\n\n if (existing) return existing;\n\n const payrollAccounts =\n payrollAccountsForWrongAccount(row);\n\n const bankAccount =\n bankAccountForWrongAccount(row);\n\n return (\n 'El empleado presenta una posible ' +\n 'inconsistencia entre la cuenta registrada ' +\n `en nómina (${payrollAccounts.join(' y ') || 'sin cuenta identificada'}) ` +\n `y la cuenta utilizada por el banco (${bankAccount || 'sin cuenta identificada'}).`\n );\n}\n\nconst metadata = data.metadata || {};\nconst summary = data.summary || {};\n\nconst rows = Array.isArray(data.rows)\n ? data.rows\n : [];\n\nconst bankWithoutBamboo =\n Array.isArray(data.bankWithoutBamboo)\n ? data.bankWithoutBamboo\n : [];\n\nconst periodLabel =\n metadata.period_label ||\n `${metadata.year || ''}-${metadata.month || ''}-${metadata.period_type || ''}`;\n\nconst periodEndLabel = formatPeriodEnd(\n metadata.period_end || ''\n);\n\nconst spreadsheetTitle =\n `Cruce de Cuentas GLM TT - ${periodLabel}`;\n\nconst sheetIds = {\n nominaVsBanco: 201,\n bancoSinNomina: 202,\n bancoSinBamboo: 203,\n diferenciasNombreBanco: 204,\n cuentaMalDigitada: 205,\n resumen: 206,\n};\n\nconst cuentaMalDigitadaCases = rows.filter(\n (row) =>\n row.category ===\n 'posible_cuenta_mal_digitada'\n);\n\nconst hasCuentaMalDigitada =\n cuentaMalDigitadaCases.length > 0;\n\nconst sheetTitles = {\n nominaVsBanco:\n '01 Nómina vs Banco',\n bancoSinNomina:\n '02 Banco sin Nómina',\n bancoSinBamboo:\n '03 Banco sin Bamboo',\n diferenciasNombreBanco:\n '04 Diferencias de Nombre',\n cuentaMalDigitada:\n '05 Cuenta Mal Digitada',\n resumen: hasCuentaMalDigitada\n ? '06 Resumen'\n : '05 Resumen',\n};\n\nconst mainRows = rows\n .filter(\n (row) =>\n row.category === 'discrepancia'\n )\n .map((row, index) => {\n const payrollAmount = roundMoney(\n row.payroll_amount ??\n row.payrollAmount ??\n 0\n );\n\n const bankAmount = roundMoney(\n row.bank_amount ??\n row.bankAmount ??\n 0\n );\n\n const difference = roundMoney(\n row.difference ??\n (payrollAmount - bankAmount)\n );\n\n return [\n index + 1,\n normalizeText(\n row.employee_name ||\n row.employee ||\n ''\n ),\n normalizeText(\n row.bank_account ||\n row.bankAccount ||\n row.payroll_account ||\n row.payrollAccount ||\n row.account ||\n ''\n ),\n payrollAmount,\n bankAmount,\n difference,\n mainReportSense(\n row,\n difference\n ),\n normalizeText(\n row.status || 'Riesgo'\n ).toUpperCase(),\n '',\n ];\n });\n\nconst bancoSinNominaRows = rows\n .filter(\n (row) =>\n row.category ===\n 'banco_sin_nomina'\n )\n .map((row, index) => [\n index + 1,\n normalizeText(\n row.employee_name ||\n row.employee ||\n ''\n ),\n normalizeText(\n row.bank_account ||\n row.bankAccount ||\n row.account ||\n ''\n ),\n roundMoney(\n row.bank_amount ??\n row.bankAmount ??\n 0\n ),\n firstValue(\n row.source_files ||\n row.source_file ||\n ''\n ),\n normalizeText(\n row.status ||\n 'Pendiente revisión'\n ).toUpperCase(),\n normalizeText(\n row.observation || ''\n ),\n '',\n ]);\n\nconst bancoSinBambooRows =\n bankWithoutBamboo.map(\n (row, index) => [\n index + 1,\n normalizeText(\n row.bank_name_file ||\n row.employee_name ||\n row.employee ||\n ''\n ),\n normalizeText(\n row.bank_account ||\n row.bankAccount ||\n row.account ||\n ''\n ),\n roundMoney(\n row.bank_amount ??\n row.bankAmount ??\n 0\n ),\n firstValue(\n row.source_files ||\n row.source_file ||\n ''\n ),\n 'PENDIENTE REVISIÓN',\n '',\n ]\n );\n\nconst diferenciasNombreRows = rows\n .filter(\n (row) =>\n row.category ===\n 'diferencia_nombre_banco'\n )\n .map((row, index) => [\n index + 1,\n normalizeText(\n row.payroll_name ||\n row.employee_name ||\n row.employee ||\n row.bank_name_file ||\n ''\n ),\n normalizeText(\n row.bank_name ||\n row.bank_account_holder ||\n ''\n ),\n normalizeText(\n row.bank_account ||\n row.bankAccount ||\n row.account ||\n ''\n ),\n roundMoney(\n row.bank_amount ??\n row.bankAmount ??\n 0\n ),\n normalizeText(\n row.status ||\n 'Pendiente revisión'\n ).toUpperCase(),\n normalizeText(\n row.observation || ''\n ),\n '',\n ]);\n\nconst cuentaMalDigitadaRows = [];\n\ncuentaMalDigitadaCases.forEach(\n (row, index) => {\n const payrollAccounts =\n payrollAccountsForWrongAccount(row);\n\n const bankAccount =\n bankAccountForWrongAccount(row);\n\n const fields = [\n [\n 'Empleado',\n normalizeText(\n row.employee_name ||\n row.employee ||\n ''\n ),\n ],\n [\n 'Cuentas registradas en las hojas de nómina',\n payrollAccounts.join(' y ') ||\n 'No se identificó una cuenta válida en la nómina.',\n ],\n [\n 'Cuenta utilizada por el banco',\n bankAccount ||\n 'No se identificó una cuenta válida en el banco.',\n ],\n [\n 'Estado del total',\n wrongAccountTotalStatus(row),\n ],\n [\n 'Hallazgo',\n wrongAccountFinding(row),\n ],\n [\n 'Clasificación',\n 'Posible cuenta mal digitada — revisar y unificar la cuenta registrada en nómina.',\n ],\n ];\n\n fields.forEach(\n (field, fieldIndex) => {\n cuentaMalDigitadaRows.push([\n fieldIndex === 0\n ? index + 1\n : '',\n field[0],\n field[1],\n '',\n ]);\n }\n );\n }\n);\n\nconst resumenRows = [\n ['Período', periodLabel],\n [\n 'Coincidencias',\n Number(summary.coincidencias || 0),\n ],\n [\n 'Discrepancias de monto o pago',\n Number(\n summary.discrepanciasMontoPago ??\n summary.discrepancias ??\n 0\n ),\n ],\n [\n 'Banco sin nómina',\n Number(summary.bancoSinNomina || 0),\n ],\n [\n 'Banco sin Bamboo',\n Number(summary.bancoSinBamboo || 0),\n ],\n [\n 'Nómina sin cuenta no conciliada',\n Number(summary.nominaSinCuenta || 0),\n ],\n [\n 'Diferencias de nombre',\n Number(\n summary.diferenciasNombreBanco || 0\n ),\n ],\n [\n 'Posibles cuentas mal digitadas',\n Number(\n summary.posiblesCuentasMalDigitadas || 0\n ),\n ],\n [\n 'Pendientes del cruce principal',\n Number(summary.pendientes || 0),\n ],\n [\n 'Empleados BambooHR Trinidad y Tobago',\n Number(summary.empleadosBambooTT || 0),\n ],\n [\n 'Empleados BambooHR en el período',\n Number(\n summary.empleadosBambooEnPeriodo || 0\n ),\n ],\n [\n 'Filas válidas de nómina',\n Number(\n summary.filasNominaValidas || 0\n ),\n ],\n [\n 'Filas de nómina sin cuenta detectadas',\n Number(\n summary.filasNominaSinCuenta || 0\n ),\n ],\n [\n 'Transacciones bancarias',\n Number(\n summary.transaccionesBanco || 0\n ),\n ],\n [\n 'Total nómina',\n roundMoney(summary.totalNomina || 0),\n ],\n [\n 'Total banco',\n roundMoney(summary.totalBanco || 0),\n ],\n [\n 'Diferencia total',\n roundMoney(\n summary.diferenciaTotal || 0\n ),\n ],\n];\n\nfunction reportValues(\n title,\n subtitle,\n header,\n body\n) {\n return [\n [\n title,\n ...Array(\n Math.max(header.length - 1, 0)\n ).fill(''),\n ],\n [\n subtitle,\n ...Array(\n Math.max(header.length - 1, 0)\n ).fill(''),\n ],\n Array(header.length).fill(''),\n header,\n ...body,\n ];\n}\n\nconst nominaVsBancoValues =\n reportValues(\n 'GOMEZLEE MARKETING',\n `Diferencias de Monto Nómina vs. Banco · Trinidad y Tobago · ${periodEndLabel}`,\n [\n '#',\n 'Empleado',\n 'Cuenta',\n 'Monto en Nómina (TT$)',\n 'Monto en Banco (TT$)',\n 'Diferencia (TT$)',\n 'Sentido',\n 'Estado',\n 'Resolución',\n ],\n mainRows\n );\n\nconst bancoSinNominaValues =\n reportValues(\n 'GOMEZLEE MARKETING',\n `Pagos bancarios sin registro en la nómina · Trinidad y Tobago · ${periodEndLabel}`,\n [\n '#',\n 'Nombre en banco',\n 'Cuenta',\n 'Monto en banco (TT$)',\n 'Archivo',\n 'Estado',\n 'Observación',\n 'Resolución',\n ],\n bancoSinNominaRows\n );\n\nconst bancoSinBambooValues =\n reportValues(\n 'GOMEZLEE MARKETING',\n `Pagos en banco sin empleado identificado en BambooHR · Trinidad y Tobago · ${periodEndLabel}`,\n [\n '#',\n 'Nombre en banco',\n 'Cuenta',\n 'Monto en banco (TT$)',\n 'Archivo',\n 'Estado',\n 'Resolución',\n ],\n bancoSinBambooRows\n );\n\nconst diferenciasNombreValues =\n reportValues(\n 'GOMEZLEE MARKETING',\n `Diferencias de nombre entre nómina y banco · Trinidad y Tobago · ${periodEndLabel}`,\n [\n '#',\n 'Nombre en nómina',\n 'Nombre en banco',\n 'Cuenta',\n 'Monto en banco (TT$)',\n 'Estado',\n 'Observación',\n 'Resolución',\n ],\n diferenciasNombreRows\n );\n\nconst cuentaMalDigitadaValues =\n reportValues(\n 'GOMEZLEE MARKETING',\n `Cuenta Mal Digitada en Nómina · Trinidad y Tobago · ${periodEndLabel}`,\n ['#', 'Campo', 'Detalle', 'Resolución'],\n cuentaMalDigitadaRows\n );\n\nconst resumenValues =\n reportValues(\n 'GOMEZLEE MARKETING',\n `Resumen del cruce Nómina vs. Banco · Trinidad y Tobago · ${periodEndLabel}`,\n ['Indicador', 'Valor'],\n resumenRows\n );\n\nconst valueData = [\n {\n range:\n `'${sheetTitles.nominaVsBanco}'!A1:I`,\n values: nominaVsBancoValues,\n },\n {\n range:\n `'${sheetTitles.bancoSinNomina}'!A1:H`,\n values: bancoSinNominaValues,\n },\n {\n range:\n `'${sheetTitles.bancoSinBamboo}'!A1:G`,\n values: bancoSinBambooValues,\n },\n {\n range:\n `'${sheetTitles.diferenciasNombreBanco}'!A1:H`,\n values: diferenciasNombreValues,\n },\n ...(hasCuentaMalDigitada\n ? [\n {\n range:\n `'${sheetTitles.cuentaMalDigitada}'!A1:D`,\n values:\n cuentaMalDigitadaValues,\n },\n ]\n : []),\n {\n range:\n `'${sheetTitles.resumen}'!A1:B`,\n values: resumenValues,\n },\n];\n\nconst brandColor = {\n red: 0.29,\n green: 0.49,\n blue: 0.58,\n};\n\nconst whiteColor = {\n red: 1,\n green: 1,\n blue: 1,\n};\n\nconst borderColor = {\n red: 0.82,\n green: 0.86,\n blue: 0.88,\n};\n\nfunction mergeRow(\n sheetId,\n rowIndex,\n columnCount\n) {\n return {\n mergeCells: {\n range: {\n sheetId,\n startRowIndex: rowIndex,\n endRowIndex: rowIndex + 1,\n startColumnIndex: 0,\n endColumnIndex: columnCount,\n },\n mergeType: 'MERGE_ALL',\n },\n };\n}\n\nfunction formatRange(\n sheetId,\n startRowIndex,\n endRowIndex,\n startColumnIndex,\n endColumnIndex,\n userEnteredFormat\n) {\n const formatFields =\n Object.keys(userEnteredFormat || {});\n\n return {\n repeatCell: {\n range: {\n sheetId,\n startRowIndex,\n endRowIndex,\n startColumnIndex,\n endColumnIndex,\n },\n cell: {\n userEnteredFormat,\n },\n fields:\n `userEnteredFormat(${formatFields.join(',')})`,\n },\n };\n}\n\nfunction titleFormat(\n sheetId,\n rowIndex,\n columnCount,\n options = {}\n) {\n return formatRange(\n sheetId,\n rowIndex,\n rowIndex + 1,\n 0,\n columnCount,\n {\n backgroundColor: brandColor,\n textFormat: {\n bold: options.bold ?? true,\n italic:\n options.italic ?? false,\n fontSize:\n options.fontSize ?? 12,\n foregroundColor:\n whiteColor,\n },\n horizontalAlignment: 'LEFT',\n verticalAlignment: 'MIDDLE',\n wrapStrategy: 'WRAP',\n }\n );\n}\n\nfunction headerFormat(\n sheetId,\n columnCount\n) {\n return formatRange(\n sheetId,\n 3,\n 4,\n 0,\n columnCount,\n {\n backgroundColor: brandColor,\n textFormat: {\n bold: true,\n foregroundColor:\n whiteColor,\n },\n horizontalAlignment: 'CENTER',\n verticalAlignment: 'MIDDLE',\n wrapStrategy: 'WRAP',\n }\n );\n}\n\nfunction freezeRows(\n sheetId,\n count\n) {\n return {\n updateSheetProperties: {\n properties: {\n sheetId,\n gridProperties: {\n frozenRowCount: count,\n },\n },\n fields:\n 'gridProperties.frozenRowCount',\n },\n };\n}\n\nfunction setFilter(\n sheetId,\n columnCount,\n endRowIndex\n) {\n return {\n setBasicFilter: {\n filter: {\n range: {\n sheetId,\n startRowIndex: 3,\n endRowIndex,\n startColumnIndex: 0,\n endColumnIndex:\n columnCount,\n },\n },\n },\n };\n}\n\nfunction setColumnWidth(\n sheetId,\n index,\n pixelSize\n) {\n return {\n updateDimensionProperties: {\n range: {\n sheetId,\n dimension: 'COLUMNS',\n startIndex: index,\n endIndex: index + 1,\n },\n properties: {\n pixelSize,\n },\n fields: 'pixelSize',\n },\n };\n}\n\nfunction setRowHeight(\n sheetId,\n startIndex,\n endIndex,\n pixelSize\n) {\n return {\n updateDimensionProperties: {\n range: {\n sheetId,\n dimension: 'ROWS',\n startIndex,\n endIndex,\n },\n properties: {\n pixelSize,\n },\n fields: 'pixelSize',\n },\n };\n}\n\nfunction borderFormat(\n sheetId,\n columnCount,\n endRowIndex\n) {\n const border = {\n style: 'SOLID',\n color: borderColor,\n };\n\n return [\n formatRange(\n sheetId,\n 3,\n endRowIndex,\n 0,\n columnCount,\n {\n verticalAlignment: 'MIDDLE',\n wrapStrategy: 'WRAP',\n }\n ),\n {\n updateBorders: {\n range: {\n sheetId,\n startRowIndex: 3,\n endRowIndex,\n startColumnIndex: 0,\n endColumnIndex: columnCount,\n },\n top: border,\n bottom: border,\n left: border,\n right: border,\n innerHorizontal: border,\n innerVertical: border,\n },\n },\n ];\n}\n\nfunction moneyFormat(\n sheetId,\n startColumnIndex,\n endColumnIndex,\n startRowIndex,\n endRowIndex\n) {\n return formatRange(\n sheetId,\n startRowIndex,\n endRowIndex,\n startColumnIndex,\n endColumnIndex,\n {\n numberFormat: {\n type: 'NUMBER',\n pattern:\n '\"TT$\"#,##0.00',\n },\n horizontalAlignment:\n 'RIGHT',\n verticalAlignment:\n 'MIDDLE',\n }\n );\n}\n\nfunction statusFormat(\n sheetId,\n columnIndex,\n endRowIndex\n) {\n return formatRange(\n sheetId,\n 4,\n endRowIndex,\n columnIndex,\n columnIndex + 1,\n {\n backgroundColor: {\n red: 1,\n green: 0.92,\n blue: 0.92,\n },\n textFormat: {\n bold: true,\n foregroundColor: {\n red: 0.82,\n green: 0.08,\n blue: 0.08,\n },\n },\n horizontalAlignment:\n 'CENTER',\n verticalAlignment:\n 'MIDDLE',\n wrapStrategy: 'WRAP',\n }\n );\n}\n\nfunction conditionalDifference(\n sheetId,\n endRowIndex,\n formula,\n backgroundColor,\n textColor\n) {\n return {\n addConditionalFormatRule: {\n rule: {\n ranges: [\n {\n sheetId,\n startRowIndex: 4,\n endRowIndex,\n startColumnIndex: 5,\n endColumnIndex: 6,\n },\n ],\n booleanRule: {\n condition: {\n type: 'CUSTOM_FORMULA',\n values: [\n {\n userEnteredValue:\n formula,\n },\n ],\n },\n format: {\n backgroundColor,\n textFormat: {\n bold: true,\n foregroundColor:\n textColor,\n },\n },\n },\n },\n index: 0,\n },\n };\n}\n\nfunction styleReport(config) {\n const {\n sheetId,\n columnCount,\n bodyRowsCount,\n widths,\n moneyColumns = [],\n statusColumn = null,\n } = config;\n\n const endRowIndex = Math.max(\n 4 + bodyRowsCount,\n 4\n );\n\n const requests = [\n mergeRow(\n sheetId,\n 0,\n columnCount\n ),\n mergeRow(\n sheetId,\n 1,\n columnCount\n ),\n titleFormat(\n sheetId,\n 0,\n columnCount,\n {\n fontSize: 12,\n bold: true,\n }\n ),\n titleFormat(\n sheetId,\n 1,\n columnCount,\n {\n fontSize: 10,\n bold: false,\n italic: true,\n }\n ),\n headerFormat(\n sheetId,\n columnCount\n ),\n freezeRows(sheetId, 4),\n setFilter(\n sheetId,\n columnCount,\n endRowIndex\n ),\n ...borderFormat(\n sheetId,\n columnCount,\n endRowIndex\n ),\n setRowHeight(\n sheetId,\n 0,\n 1,\n 30\n ),\n setRowHeight(\n sheetId,\n 1,\n 2,\n 28\n ),\n setRowHeight(\n sheetId,\n 3,\n 4,\n 42\n ),\n ...widths.map(\n (width, index) =>\n setColumnWidth(\n sheetId,\n index,\n width\n )\n ),\n ];\n\n if (bodyRowsCount > 0) {\n requests.push(\n setRowHeight(\n sheetId,\n 4,\n endRowIndex,\n 30\n )\n );\n\n for (\n const [startColumn, endColumn] of\n moneyColumns\n ) {\n requests.push(\n moneyFormat(\n sheetId,\n startColumn,\n endColumn,\n 4,\n endRowIndex\n )\n );\n }\n\n if (\n Number.isInteger(\n statusColumn\n )\n ) {\n requests.push(\n statusFormat(\n sheetId,\n statusColumn,\n endRowIndex\n )\n );\n }\n }\n\n return requests;\n}\n\n\nfunction wrapRangeRequest(\n sheetId,\n startRowIndex,\n endRowIndex,\n startColumnIndex,\n endColumnIndex\n) {\n return {\n repeatCell: {\n range: {\n sheetId,\n startRowIndex,\n endRowIndex,\n startColumnIndex,\n endColumnIndex,\n },\n cell: {\n userEnteredFormat: {\n verticalAlignment: 'MIDDLE',\n wrapStrategy: 'WRAP',\n },\n },\n fields:\n 'userEnteredFormat(verticalAlignment,wrapStrategy)',\n },\n };\n}\n\nfunction autoResizeRowsRequest(\n sheetId,\n startIndex,\n endIndex\n) {\n return {\n autoResizeDimensions: {\n dimensions: {\n sheetId,\n dimension: 'ROWS',\n startIndex,\n endIndex,\n },\n },\n };\n}\n\nconst formatRequests = [\n ...styleReport({\n sheetId:\n sheetIds.nominaVsBanco,\n columnCount: 9,\n bodyRowsCount:\n mainRows.length,\n widths: [\n 48,\n 250,\n 145,\n 135,\n 135,\n 135,\n 180,\n 120,\n 260,\n ],\n moneyColumns: [\n [3, 6],\n ],\n statusColumn: 7,\n }),\n\n ...(mainRows.length > 0\n ? [\n conditionalDifference(\n sheetIds.nominaVsBanco,\n 4 + mainRows.length,\n '=$F5>0',\n {\n red: 1,\n green: 0.97,\n blue: 0.82,\n },\n {\n red: 0.45,\n green: 0.27,\n blue: 0,\n }\n ),\n conditionalDifference(\n sheetIds.nominaVsBanco,\n 4 + mainRows.length,\n '=$F5<0',\n {\n red: 1,\n green: 0.89,\n blue: 0.89,\n },\n {\n red: 0.85,\n green: 0.05,\n blue: 0.05,\n }\n ),\n ]\n : []),\n\n ...styleReport({\n sheetId:\n sheetIds.bancoSinNomina,\n columnCount: 8,\n bodyRowsCount:\n bancoSinNominaRows.length,\n widths: [\n 48,\n 230,\n 145,\n 135,\n 230,\n 140,\n 360,\n 260,\n ],\n moneyColumns: [[3, 4]],\n statusColumn: 5,\n }),\n\n ...styleReport({\n sheetId:\n sheetIds.bancoSinBamboo,\n columnCount: 7,\n bodyRowsCount:\n bancoSinBambooRows.length,\n widths: [\n 48,\n 250,\n 145,\n 140,\n 250,\n 150,\n 260,\n ],\n moneyColumns: [[3, 4]],\n statusColumn: 5,\n }),\n\n ...styleReport({\n sheetId:\n sheetIds.diferenciasNombreBanco,\n columnCount: 8,\n bodyRowsCount:\n diferenciasNombreRows.length,\n widths: [\n 48,\n 240,\n 240,\n 145,\n 140,\n 150,\n 420,\n 260,\n ],\n moneyColumns: [[4, 5]],\n statusColumn: 5,\n }),\n];\n\nif (hasCuentaMalDigitada) {\n const endRowIndex =\n 4 +\n cuentaMalDigitadaRows.length;\n\n formatRequests.push(\n ...styleReport({\n sheetId:\n sheetIds.cuentaMalDigitada,\n columnCount: 4,\n bodyRowsCount:\n cuentaMalDigitadaRows.length,\n widths: [\n 48,\n 300,\n 520,\n 260,\n ],\n statusColumn: null,\n })\n );\n\n cuentaMalDigitadaCases.forEach(\n (_, caseIndex) => {\n const startRowIndex =\n 4 + caseIndex * 6;\n\n const endCaseRowIndex =\n startRowIndex + 6;\n\n formatRequests.push(\n {\n mergeCells: {\n range: {\n sheetId:\n sheetIds.cuentaMalDigitada,\n startRowIndex,\n endRowIndex:\n endCaseRowIndex,\n startColumnIndex: 0,\n endColumnIndex: 1,\n },\n mergeType:\n 'MERGE_ALL',\n },\n },\n {\n mergeCells: {\n range: {\n sheetId:\n sheetIds.cuentaMalDigitada,\n startRowIndex,\n endRowIndex:\n endCaseRowIndex,\n startColumnIndex: 3,\n endColumnIndex: 4,\n },\n mergeType:\n 'MERGE_ALL',\n },\n },\n formatRange(\n sheetIds.cuentaMalDigitada,\n startRowIndex,\n endCaseRowIndex,\n 0,\n 1,\n {\n backgroundColor: {\n red: 0.91,\n green: 0.95,\n blue: 0.99,\n },\n textFormat: {\n bold: true,\n foregroundColor: {\n red: 0.20,\n green: 0.36,\n blue: 0.45,\n },\n },\n horizontalAlignment:\n 'CENTER',\n verticalAlignment:\n 'MIDDLE',\n wrapStrategy: 'WRAP',\n }\n ),\n formatRange(\n sheetIds.cuentaMalDigitada,\n startRowIndex,\n endCaseRowIndex,\n 1,\n 2,\n {\n backgroundColor: {\n red: 0.93,\n green: 0.97,\n blue: 0.90,\n },\n textFormat: {\n bold: true,\n foregroundColor: {\n red: 0.20,\n green: 0.36,\n blue: 0.45,\n },\n },\n horizontalAlignment:\n 'LEFT',\n verticalAlignment:\n 'MIDDLE',\n wrapStrategy: 'WRAP',\n }\n ),\n formatRange(\n sheetIds.cuentaMalDigitada,\n startRowIndex,\n endCaseRowIndex,\n 2,\n 3,\n {\n horizontalAlignment:\n 'LEFT',\n verticalAlignment:\n 'MIDDLE',\n wrapStrategy: 'WRAP',\n }\n ),\n setRowHeight(\n sheetIds.cuentaMalDigitada,\n startRowIndex,\n endCaseRowIndex,\n 52\n )\n );\n }\n );\n}\n\nconst resumenEndRowIndex =\n 4 + resumenRows.length;\n\nformatRequests.push(\n ...styleReport({\n sheetId:\n sheetIds.resumen,\n columnCount: 2,\n bodyRowsCount:\n resumenRows.length,\n widths: [340, 180],\n statusColumn: null,\n }),\n moneyFormat(\n sheetIds.resumen,\n 1,\n 2,\n resumenEndRowIndex - 3,\n resumenEndRowIndex\n )\n);\n\n\nconst mainReadabilityEndRow =\n 4 + mainRows.length;\n\nconst bancoSinNominaReadabilityEndRow =\n 4 + bancoSinNominaRows.length;\n\nconst bancoSinBambooReadabilityEndRow =\n 4 + bancoSinBambooRows.length;\n\nconst diferenciasNombreReadabilityEndRow =\n 4 + diferenciasNombreRows.length;\n\nconst resumenReadabilityEndRow =\n 4 + resumenRows.length;\n\n/*\n * Ajuste final de legibilidad.\n *\n * Los anchos definitivos se aplican antes del autoajuste vertical. De esta\n * forma Google Sheets calcula la altura real de cada fila después de envolver\n * el texto, evitando contenido cortado en nombres, observaciones y resúmenes.\n */\nformatRequests.push(\n // 01 Nómina vs Banco\n setColumnWidth(\n sheetIds.nominaVsBanco,\n 1,\n 300\n ),\n setColumnWidth(\n sheetIds.nominaVsBanco,\n 2,\n 150\n ),\n setColumnWidth(\n sheetIds.nominaVsBanco,\n 3,\n 145\n ),\n setColumnWidth(\n sheetIds.nominaVsBanco,\n 4,\n 145\n ),\n setColumnWidth(\n sheetIds.nominaVsBanco,\n 5,\n 145\n ),\n setColumnWidth(\n sheetIds.nominaVsBanco,\n 6,\n 220\n ),\n setColumnWidth(\n sheetIds.nominaVsBanco,\n 7,\n 140\n ),\n setColumnWidth(\n sheetIds.nominaVsBanco,\n 8,\n 320\n ),\n ...(mainRows.length > 0\n ? [\n wrapRangeRequest(\n sheetIds.nominaVsBanco,\n 4,\n mainReadabilityEndRow,\n 0,\n 9\n ),\n autoResizeRowsRequest(\n sheetIds.nominaVsBanco,\n 4,\n mainReadabilityEndRow\n ),\n ]\n : []),\n\n // 02 Banco sin Nómina\n setColumnWidth(\n sheetIds.bancoSinNomina,\n 1,\n 320\n ),\n setColumnWidth(\n sheetIds.bancoSinNomina,\n 2,\n 160\n ),\n setColumnWidth(\n sheetIds.bancoSinNomina,\n 3,\n 145\n ),\n setColumnWidth(\n sheetIds.bancoSinNomina,\n 4,\n 260\n ),\n setColumnWidth(\n sheetIds.bancoSinNomina,\n 5,\n 170\n ),\n setColumnWidth(\n sheetIds.bancoSinNomina,\n 6,\n 560\n ),\n setColumnWidth(\n sheetIds.bancoSinNomina,\n 7,\n 320\n ),\n ...(bancoSinNominaRows.length > 0\n ? [\n wrapRangeRequest(\n sheetIds.bancoSinNomina,\n 4,\n bancoSinNominaReadabilityEndRow,\n 0,\n 8\n ),\n autoResizeRowsRequest(\n sheetIds.bancoSinNomina,\n 4,\n bancoSinNominaReadabilityEndRow\n ),\n ]\n : []),\n\n // 03 Banco sin Bamboo\n setColumnWidth(\n sheetIds.bancoSinBamboo,\n 1,\n 320\n ),\n setColumnWidth(\n sheetIds.bancoSinBamboo,\n 2,\n 160\n ),\n setColumnWidth(\n sheetIds.bancoSinBamboo,\n 3,\n 145\n ),\n setColumnWidth(\n sheetIds.bancoSinBamboo,\n 4,\n 260\n ),\n setColumnWidth(\n sheetIds.bancoSinBamboo,\n 5,\n 170\n ),\n setColumnWidth(\n sheetIds.bancoSinBamboo,\n 6,\n 320\n ),\n ...(bancoSinBambooRows.length > 0\n ? [\n wrapRangeRequest(\n sheetIds.bancoSinBamboo,\n 4,\n bancoSinBambooReadabilityEndRow,\n 0,\n 7\n ),\n autoResizeRowsRequest(\n sheetIds.bancoSinBamboo,\n 4,\n bancoSinBambooReadabilityEndRow\n ),\n ]\n : []),\n\n // 04 Diferencias de Nombre\n setColumnWidth(\n sheetIds.diferenciasNombreBanco,\n 1,\n 320\n ),\n setColumnWidth(\n sheetIds.diferenciasNombreBanco,\n 2,\n 320\n ),\n setColumnWidth(\n sheetIds.diferenciasNombreBanco,\n 3,\n 160\n ),\n setColumnWidth(\n sheetIds.diferenciasNombreBanco,\n 4,\n 145\n ),\n setColumnWidth(\n sheetIds.diferenciasNombreBanco,\n 5,\n 170\n ),\n setColumnWidth(\n sheetIds.diferenciasNombreBanco,\n 6,\n 600\n ),\n setColumnWidth(\n sheetIds.diferenciasNombreBanco,\n 7,\n 320\n ),\n ...(diferenciasNombreRows.length > 0\n ? [\n wrapRangeRequest(\n sheetIds.diferenciasNombreBanco,\n 4,\n diferenciasNombreReadabilityEndRow,\n 0,\n 8\n ),\n autoResizeRowsRequest(\n sheetIds.diferenciasNombreBanco,\n 4,\n diferenciasNombreReadabilityEndRow\n ),\n ]\n : []),\n\n // 05 Cuenta Mal Digitada\n ...(hasCuentaMalDigitada\n ? [\n setColumnWidth(\n sheetIds.cuentaMalDigitada,\n 1,\n 300\n ),\n setColumnWidth(\n sheetIds.cuentaMalDigitada,\n 2,\n 600\n ),\n setColumnWidth(\n sheetIds.cuentaMalDigitada,\n 3,\n 320\n ),\n ]\n : []),\n\n // Resumen\n setColumnWidth(\n sheetIds.resumen,\n 0,\n 380\n ),\n setColumnWidth(\n sheetIds.resumen,\n 1,\n 320\n ),\n ...(resumenRows.length > 0\n ? [\n wrapRangeRequest(\n sheetIds.resumen,\n 4,\n resumenReadabilityEndRow,\n 0,\n 2\n ),\n autoResizeRowsRequest(\n sheetIds.resumen,\n 4,\n resumenReadabilityEndRow\n ),\n ]\n : [])\n);\n\n\nreturn [\n {\n json: {\n ok: true,\n stage:\n 'preparar_google_sheet_tt',\n metadata,\n summary,\n spreadsheetTitle,\n sheetIds,\n sheetTitles,\n createSpreadsheetBody: {\n properties: {\n title: spreadsheetTitle,\n },\n sheets: [\n {\n properties: {\n sheetId:\n sheetIds.nominaVsBanco,\n title:\n sheetTitles.nominaVsBanco,\n },\n },\n {\n properties: {\n sheetId:\n sheetIds.bancoSinNomina,\n title:\n sheetTitles.bancoSinNomina,\n },\n },\n {\n properties: {\n sheetId:\n sheetIds.bancoSinBamboo,\n title:\n sheetTitles.bancoSinBamboo,\n },\n },\n {\n properties: {\n sheetId:\n sheetIds.diferenciasNombreBanco,\n title:\n sheetTitles.diferenciasNombreBanco,\n },\n },\n ...(hasCuentaMalDigitada\n ? [\n {\n properties: {\n sheetId:\n sheetIds.cuentaMalDigitada,\n title:\n sheetTitles.cuentaMalDigitada,\n },\n },\n ]\n : []),\n {\n properties: {\n sheetId:\n sheetIds.resumen,\n title:\n sheetTitles.resumen,\n },\n },\n ],\n },\n valueBatchBody: {\n valueInputOption:\n 'RAW',\n data: valueData,\n },\n formatBatchBody: {\n requests: formatRequests,\n },\n originalResponse: data,\n },\n },\n];" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 7616, - 6368 + 7264, + 7296 ], - "id": "9ab423de-de88-465c-90da-55f88097fb9d", + "id": "0269ecb0-63b1-43f6-9f34-d18d97a104b4", "name": "Preparar Google Sheet" }, { @@ -406,10 +406,10 @@ "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.4, "position": [ - 7872, - 6368 + 7520, + 7296 ], - "id": "981f50d3-0594-4dca-90f1-dfb6b5d38d60", + "id": "1ef3b281-554e-41dd-b95f-28fa3a1e70be", "name": "Crear Google Sheet", "credentials": { "httpBasicAuth": { @@ -436,10 +436,10 @@ "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.4, "position": [ - 8128, - 6368 + 7776, + 7296 ], - "id": "8230bd87-f59c-4df9-86e9-cc55f18111fc", + "id": "cf220e3d-0954-4b08-aa48-7d90afbb099d", "name": "Escribir Google Sheet", "credentials": { "googleOAuth2Api": { @@ -462,10 +462,10 @@ "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.4, "position": [ - 8384, - 6368 + 8032, + 7296 ], - "id": "007569d6-9d90-40e6-8fac-05dc52c2e7b6", + "id": "1e76a3ba-00f2-4455-ac64-e1f377e3d309", "name": "Formatear Google Sheet", "credentials": { "googleOAuth2Api": { @@ -481,10 +481,10 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 8656, - 6368 + 8304, + 7296 ], - "id": "e9a34bf2-dfd2-419d-adad-8d19e4249524", + "id": "bba45803-df53-41eb-9f04-988fc9456a3c", "name": "Preparar permisos Google Sheet" }, { @@ -501,10 +501,10 @@ "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.4, "position": [ - 8912, - 6368 + 8560, + 7296 ], - "id": "fe5677e2-b62e-483f-85ed-febbd122944a", + "id": "00d29e58-0e73-4b96-8848-f3012b30189b", "name": "Compartir Google Sheet", "credentials": { "googleOAuth2Api": { @@ -520,10 +520,10 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 10256, - 6368 + 9904, + 7296 ], - "id": "a238b381-4aeb-4be2-a791-d12e1de986f8", + "id": "09cfaf1e-f278-4f34-b3f2-09ccbc37c10d", "name": "Preparar histórico Supabase" }, { @@ -559,10 +559,10 @@ "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.4, "position": [ - 10512, - 6368 + 10160, + 7296 ], - "id": "67e475c5-d8b6-4a7f-a5f3-b195c53bd1ea", + "id": "a828f0bf-44ef-452c-8aae-f089b34aace5", "name": "Insertar histórico Supabase", "onError": "continueRegularOutput" }, @@ -573,10 +573,10 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 10784, - 6368 + 10432, + 7296 ], - "id": "b1fffa33-91ed-489d-8875-d4ed95a10288", + "id": "4321d98f-7d24-49d5-a03f-b19eb407f11e", "name": "Preparar respuesta final" }, { @@ -598,10 +598,10 @@ "type": "n8n-nodes-base.respondToWebhook", "typeVersion": 1.5, "position": [ - 11040, - 6368 + 10688, + 7296 ], - "id": "c6d47f4e-8e5e-4be9-bf7e-3f0b44065b69", + "id": "5f85312f-d57b-462b-943a-a7339f81e69d", "name": "Respond to Webhook" }, { @@ -614,10 +614,10 @@ "type": "n8n-nodes-base.stickyNote", "typeVersion": 1, "position": [ - 2928, - 5696 + 2576, + 6624 ], - "id": "6ae02113-7126-4271-be54-768996b76ca1", + "id": "d209ed03-9a6c-4377-aeb3-a25361644e5f", "name": "Sticky Note" }, { @@ -630,10 +630,10 @@ "type": "n8n-nodes-base.stickyNote", "typeVersion": 1, "position": [ - 4880, - 5632 + 4528, + 6560 ], - "id": "379d7a07-eb53-47c2-9223-33c2cf12cc00", + "id": "17197e7f-e073-464e-b8c8-ff0856c584d7", "name": "Sticky Note1" }, { @@ -646,10 +646,10 @@ "type": "n8n-nodes-base.stickyNote", "typeVersion": 1, "position": [ - 7104, - 6016 + 6752, + 6944 ], - "id": "c180d1f6-f9f9-41c9-b471-972062de1f50", + "id": "656ae2c7-4c04-4346-90de-1e4db21a637d", "name": "Sticky Note2" }, { @@ -662,10 +662,10 @@ "type": "n8n-nodes-base.stickyNote", "typeVersion": 1, "position": [ - 9296, - 6016 + 8944, + 6944 ], - "id": "20a6ed17-e087-4dca-b528-a9e076b9c9c7", + "id": "ac5189a6-802b-4f5e-a946-2aa574408974", "name": "Sticky Note3" } ], @@ -1063,9 +1063,9 @@ }, "staticData": null, "meta": null, - "versionId": "cd06be43-5481-4a32-b10d-eb7434fab10d", - "activeVersionId": "cd06be43-5481-4a32-b10d-eb7434fab10d", - "versionCounter": 111, + "versionId": "2be11ec0-4659-47ca-9a58-38fd0f9eb4d9", + "activeVersionId": "2be11ec0-4659-47ca-9a58-38fd0f9eb4d9", + "versionCounter": 115, "triggerCount": 1, "shared": [ { @@ -1088,9 +1088,9 @@ ], "tags": [], "activeVersion": { - "updatedAt": "2026-07-22T15:11:27.000Z", - "createdAt": "2026-07-22T15:11:25.948Z", - "versionId": "cd06be43-5481-4a32-b10d-eb7434fab10d", + "updatedAt": "2026-07-27T15:01:27.000Z", + "createdAt": "2026-07-27T15:01:25.673Z", + "versionId": "2be11ec0-4659-47ca-9a58-38fd0f9eb4d9", "workflowId": "5AujMxduslftVg9z", "nodes": [ { @@ -1103,10 +1103,10 @@ "type": "n8n-nodes-base.webhook", "typeVersion": 2.1, "position": [ - 3200, - 6416 + 2848, + 7344 ], - "id": "74d32f2c-d24e-402c-9af8-5aceb5664112", + "id": "0a3764cc-5cc2-4c62-93f8-1d201ec45e9d", "name": "Webhook", "webhookId": "9c730860-7790-43a5-a3c0-bf5984ced244" }, @@ -1117,10 +1117,10 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 3440, - 6416 + 3088, + 7344 ], - "id": "79c8b752-1397-4235-8b22-902c35feb488", + "id": "bdfa28aa-5a9e-4c08-9869-0bafa2a8cb52", "name": "Preparar entrada app" }, { @@ -1130,10 +1130,10 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 3776, - 6112 + 3424, + 7040 ], - "id": "8b00e487-ac48-4d19-a66e-4a722d57ba58", + "id": "46e46f3c-85f9-40ef-a3cc-ee20acc46d73", "name": "Parsear CSV banco TT" }, { @@ -1148,10 +1148,10 @@ "type": "n8n-nodes-base.extractFromFile", "typeVersion": 1.1, "position": [ - 3776, - 6512 + 3424, + 7440 ], - "id": "be8b25e6-db83-4b01-8d3e-1a0635481833", + "id": "444192a4-bd82-4086-a87f-ab116517f723", "name": "Extract - BICE", "retryOnFail": false }, @@ -1167,10 +1167,10 @@ "type": "n8n-nodes-base.extractFromFile", "typeVersion": 1.1, "position": [ - 3776, - 6688 + 3424, + 7616 ], - "id": "9bda9647-b34e-4cba-9ec5-88b3e758e084", + "id": "15b0af6e-5e38-4c0c-9d30-496ad9df9413", "name": "Extract - Goldey Samuel", "retryOnFail": false }, @@ -1186,10 +1186,10 @@ "type": "n8n-nodes-base.extractFromFile", "typeVersion": 1.1, "position": [ - 3776, - 6848 + 3424, + 7776 ], - "id": "3fdc27b6-e651-4a3c-9654-d554e1affd7a", + "id": "22845609-e9b5-486b-88ce-bc5d73b96a2e", "name": "Extract - P&G", "retryOnFail": false }, @@ -1205,10 +1205,10 @@ "type": "n8n-nodes-base.extractFromFile", "typeVersion": 1.1, "position": [ - 3776, - 7024 + 3424, + 7952 ], - "id": "4ed66d6e-9ca0-430a-8361-71ee2f7bbf98", + "id": "9a39edbc-1e2e-4d75-83ed-9ce48c808abf", "name": "Extract - Whirlpool", "retryOnFail": false }, @@ -1224,10 +1224,10 @@ "type": "n8n-nodes-base.extractFromFile", "typeVersion": 1.1, "position": [ - 3776, - 7200 + 3424, + 8128 ], - "id": "da474228-1954-4ea2-9ce6-b1880754dca0", + "id": "1710469a-3b4a-4a3b-9c61-77d7d3c4fffb", "name": "Extract - KAD", "retryOnFail": false }, @@ -1243,10 +1243,10 @@ "type": "n8n-nodes-base.extractFromFile", "typeVersion": 1.1, "position": [ - 3776, - 7360 + 3424, + 8288 ], - "id": "34fb614f-9c2e-4d29-9296-7ac76b21c097", + "id": "72c876ce-f1e4-4f82-ac7d-7d409eb18e64", "name": "Extract - GLM People", "retryOnFail": false }, @@ -1262,10 +1262,10 @@ "type": "n8n-nodes-base.extractFromFile", "typeVersion": 1.1, "position": [ - 3776, - 7536 + 3424, + 8464 ], - "id": "25847676-a237-4471-931a-c71f35b690b7", + "id": "26d185f0-3f60-44cd-b20e-1fbfbae48fc8", "name": "Extract - GLM", "retryOnFail": false }, @@ -1274,10 +1274,10 @@ "type": "n8n-nodes-base.merge", "typeVersion": 3.2, "position": [ - 5440, - 6608 + 5088, + 7536 ], - "id": "f2ad5ad8-120f-40fe-b078-97b979d7f2e1", + "id": "b98e1002-cf47-4cde-94d7-08777b928d36", "name": "Merge Hojas TT 01-02" }, { @@ -1285,10 +1285,10 @@ "type": "n8n-nodes-base.merge", "typeVersion": 3.2, "position": [ - 5440, - 6768 + 5088, + 7696 ], - "id": "6d358ee2-9317-40d1-8171-5cb536736300", + "id": "c7c11485-0d25-4959-ba19-cf631591472a", "name": "Merge Hojas TT 03" }, { @@ -1296,10 +1296,10 @@ "type": "n8n-nodes-base.merge", "typeVersion": 3.2, "position": [ - 5440, - 6944 + 5088, + 7872 ], - "id": "8da68511-831e-47f3-a5d6-6d25183caac1", + "id": "706b5c48-c1f0-4a12-be90-51abd68f32f3", "name": "Merge Hojas TT 04" }, { @@ -1307,10 +1307,10 @@ "type": "n8n-nodes-base.merge", "typeVersion": 3.2, "position": [ - 5440, - 7104 + 5088, + 8032 ], - "id": "e8527452-f631-44e7-b37a-5be8bdb0f443", + "id": "a6cff463-cf7d-4e70-b13d-213dbcefa388", "name": "Merge Hojas TT 05" }, { @@ -1318,10 +1318,10 @@ "type": "n8n-nodes-base.merge", "typeVersion": 3.2, "position": [ - 5440, - 7280 + 5088, + 8208 ], - "id": "7b96730c-826c-4076-993b-5b55a2396f49", + "id": "25f84f87-c966-4b81-ac10-9021b7eeadc3", "name": "Merge Hojas TT 06" }, { @@ -1329,10 +1329,10 @@ "type": "n8n-nodes-base.merge", "typeVersion": 3.2, "position": [ - 5440, - 7456 + 5088, + 8384 ], - "id": "17bec528-8fcd-41e1-a485-00ea6d1f7e5f", + "id": "75156223-c7d3-4b98-bdc5-aa62a9185db1", "name": "Merge Hojas TT 07" }, { @@ -1342,10 +1342,10 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 5808, - 7024 + 5456, + 7952 ], - "id": "e960c9ca-88dd-4388-8da2-b8575360ed44", + "id": "d036e80f-bf9c-4024-9bdb-f9222d8ee057", "name": "Normalizar Nómina TT" }, { @@ -1357,10 +1357,10 @@ "type": "n8n-nodes-base.merge", "typeVersion": 3.2, "position": [ - 6064, - 6368 + 5712, + 7296 ], - "id": "f053d7f4-f031-4379-8999-48c47ebcec37", + "id": "8d2116b6-d720-47a0-b532-aec6f16966c1", "name": "Merge Banco + Nómina TT" }, { @@ -1409,10 +1409,10 @@ "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.4, "position": [ - 3776, - 5888 + 3424, + 6816 ], - "id": "ddb78ab5-fdf3-45a0-b069-01adead3ad87", + "id": "84b74d83-a969-4f62-a11b-ace145d64e8c", "name": "HTTP - Empleados BambooHR TT", "retryOnFail": true, "maxTries": 3, @@ -1431,10 +1431,10 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 5440, - 5888 + 5088, + 6816 ], - "id": "713fdd46-81bb-4a0b-b75c-47ef54371a1b", + "id": "d6d947b2-4a0e-4917-855c-7b69a27dae4e", "name": "Normalizar BambooHR TT" }, { @@ -1446,10 +1446,10 @@ "type": "n8n-nodes-base.merge", "typeVersion": 3.2, "position": [ - 6320, - 6368 + 5968, + 7296 ], - "id": "2b803f96-0829-430e-bef8-4db170337ad4", + "id": "9760d7dd-776e-4ae7-b56b-2f8ac85cc768", "name": "Merge - Agregar BambooHR TT" }, { @@ -1459,23 +1459,23 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 6576, - 6368 + 6224, + 7296 ], - "id": "53f3881a-3cf3-44a3-b68c-619aae022b1d", + "id": "ea4d4d89-a2f9-4c3f-9c53-803706173b27", "name": "Cruzar Nómina vs Banco" }, { "parameters": { - "jsCode": "const data = $input.first().json || {};\n\nfunction normalizeText(value) {\n return String(value ?? '')\n .replace(/\\uFEFF/g, '')\n .replace(/\\u00A0/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim();\n}\n\nfunction roundMoney(value) {\n return Math.round(\n (Number(value) || 0) * 100\n ) / 100;\n}\n\nfunction firstValue(value) {\n if (Array.isArray(value)) {\n return value\n .map(normalizeText)\n .filter(Boolean)\n .join(' / ');\n }\n\n return normalizeText(value);\n}\n\nfunction formatPeriodEnd(value) {\n const raw = normalizeText(value);\n\n if (!/^\\d{4}-\\d{2}-\\d{2}$/.test(raw)) {\n return raw;\n }\n\n const [year, month, day] = raw.split('-');\n\n const monthNames = {\n '01': 'ene',\n '02': 'feb',\n '03': 'mar',\n '04': 'abr',\n '05': 'may',\n '06': 'jun',\n '07': 'jul',\n '08': 'ago',\n '09': 'sep',\n '10': 'oct',\n '11': 'nov',\n '12': 'dic',\n };\n\n return `${day}-${monthNames[month] || month}-${year}`;\n}\n\nfunction mainReportSense(row, difference) {\n const subcategory = normalizeText(\n row.subcategory\n ).toLowerCase();\n\n const payrollAmount = Number(\n row.payroll_amount ??\n row.payrollAmount ??\n 0\n );\n\n const bankAmount = Number(\n row.bank_amount ??\n row.bankAmount ??\n 0\n );\n\n if (\n subcategory ===\n 'nomina_con_cuenta_sin_pago_banco' ||\n (bankAmount === 0 && payrollAmount > 0)\n ) {\n return 'No aparece pagado en banco';\n }\n\n if (difference > 0) {\n return 'Se pagó de menos';\n }\n\n if (difference < 0) {\n return 'Se pagó de más';\n }\n\n return 'Revisar';\n}\n\nfunction accountValues(value) {\n const values = Array.isArray(value)\n ? value\n : String(value ?? '')\n .split(/\\s*(?:\\/|;|,|\\by\\b)\\s*/i);\n\n return values\n .map((item) =>\n String(item ?? '')\n .replace(/\\u00A0/g, '')\n .replace(/\\.0$/g, '')\n .replace(/\\D/g, '')\n .trim()\n )\n .filter(\n (account) =>\n account.length >= 6 &&\n !/^0+$/.test(account)\n );\n}\n\nfunction payrollAccountsForWrongAccount(row) {\n const candidates = [\n row.payroll_account,\n row.payrollAccount,\n ...(Array.isArray(row.source_rows)\n ? row.source_rows.flatMap(\n (sourceRow) => [\n sourceRow.account,\n sourceRow.payroll_account,\n sourceRow.payrollAccount,\n ]\n )\n : []),\n ];\n\n return Array.from(\n new Set(\n candidates.flatMap(accountValues)\n )\n );\n}\n\nfunction bankAccountForWrongAccount(row) {\n return firstValue(\n row.bank_account ||\n row.bankAccount ||\n row.account ||\n ''\n );\n}\n\nfunction moneyLabel(value) {\n return Math.abs(\n roundMoney(value)\n ).toLocaleString('en-US', {\n minimumFractionDigits: 2,\n maximumFractionDigits: 2,\n });\n}\n\nfunction wrongAccountTotalStatus(row) {\n const payrollAmount = roundMoney(\n row.payroll_amount ??\n row.payrollAmount ??\n 0\n );\n\n const bankAmount = roundMoney(\n row.bank_amount ??\n row.bankAmount ??\n 0\n );\n\n const difference = roundMoney(\n payrollAmount - bankAmount\n );\n\n if (Math.abs(difference) <= 0.02) {\n return (\n 'El total de nómina coincide con ' +\n 'el total pagado por el banco.'\n );\n }\n\n if (difference > 0) {\n return (\n 'El total de nómina supera el total ' +\n `del banco por TT$${moneyLabel(difference)}.`\n );\n }\n\n return (\n 'El total pagado por el banco supera ' +\n `el total de nómina por TT$${moneyLabel(difference)}.`\n );\n}\n\nfunction wrongAccountFinding(row) {\n const existing = normalizeText(\n row.observation || ''\n );\n\n if (existing) return existing;\n\n const payrollAccounts =\n payrollAccountsForWrongAccount(row);\n\n const bankAccount =\n bankAccountForWrongAccount(row);\n\n return (\n 'El empleado presenta una posible ' +\n 'inconsistencia entre la cuenta registrada ' +\n `en nómina (${payrollAccounts.join(' y ') || 'sin cuenta identificada'}) ` +\n `y la cuenta utilizada por el banco (${bankAccount || 'sin cuenta identificada'}).`\n );\n}\n\nconst metadata = data.metadata || {};\nconst summary = data.summary || {};\n\nconst rows = Array.isArray(data.rows)\n ? data.rows\n : [];\n\nconst bankWithoutBamboo =\n Array.isArray(data.bankWithoutBamboo)\n ? data.bankWithoutBamboo\n : [];\n\nconst periodLabel =\n metadata.period_label ||\n `${metadata.year || ''}-${metadata.month || ''}-${metadata.period_type || ''}`;\n\nconst periodEndLabel = formatPeriodEnd(\n metadata.period_end || ''\n);\n\nconst spreadsheetTitle =\n `Cruce de Cuentas GLM TT - ${periodLabel}`;\n\nconst sheetIds = {\n nominaVsBanco: 201,\n bancoSinNomina: 202,\n bancoSinBamboo: 203,\n diferenciasNombreBanco: 204,\n cuentaMalDigitada: 205,\n resumen: 206,\n};\n\nconst cuentaMalDigitadaCases = rows.filter(\n (row) =>\n row.category ===\n 'posible_cuenta_mal_digitada'\n);\n\nconst hasCuentaMalDigitada =\n cuentaMalDigitadaCases.length > 0;\n\nconst sheetTitles = {\n nominaVsBanco:\n '01 Nómina vs Banco',\n bancoSinNomina:\n '02 Banco sin Nómina',\n bancoSinBamboo:\n '03 Banco sin Bamboo',\n diferenciasNombreBanco:\n '04 Diferencias de Nombre',\n cuentaMalDigitada:\n '05 Cuenta Mal Digitada',\n resumen: hasCuentaMalDigitada\n ? '06 Resumen'\n : '05 Resumen',\n};\n\nconst mainRows = rows\n .filter(\n (row) =>\n row.category === 'discrepancia'\n )\n .map((row, index) => {\n const payrollAmount = roundMoney(\n row.payroll_amount ??\n row.payrollAmount ??\n 0\n );\n\n const bankAmount = roundMoney(\n row.bank_amount ??\n row.bankAmount ??\n 0\n );\n\n const difference = roundMoney(\n row.difference ??\n (payrollAmount - bankAmount)\n );\n\n return [\n index + 1,\n normalizeText(\n row.employee_name ||\n row.employee ||\n ''\n ),\n normalizeText(\n row.bank_account ||\n row.bankAccount ||\n row.payroll_account ||\n row.payrollAccount ||\n row.account ||\n ''\n ),\n payrollAmount,\n bankAmount,\n difference,\n mainReportSense(\n row,\n difference\n ),\n normalizeText(\n row.status || 'Riesgo'\n ).toUpperCase(),\n '',\n ];\n });\n\nconst bancoSinNominaRows = rows\n .filter(\n (row) =>\n row.category ===\n 'banco_sin_nomina'\n )\n .map((row, index) => [\n index + 1,\n normalizeText(\n row.employee_name ||\n row.employee ||\n ''\n ),\n normalizeText(\n row.bank_account ||\n row.bankAccount ||\n row.account ||\n ''\n ),\n roundMoney(\n row.bank_amount ??\n row.bankAmount ??\n 0\n ),\n firstValue(\n row.source_files ||\n row.source_file ||\n ''\n ),\n normalizeText(\n row.status ||\n 'Pendiente revisión'\n ).toUpperCase(),\n normalizeText(\n row.observation || ''\n ),\n '',\n ]);\n\nconst bancoSinBambooRows =\n bankWithoutBamboo.map(\n (row, index) => [\n index + 1,\n normalizeText(\n row.bank_name_file ||\n row.employee_name ||\n row.employee ||\n ''\n ),\n normalizeText(\n row.bank_account ||\n row.bankAccount ||\n row.account ||\n ''\n ),\n roundMoney(\n row.bank_amount ??\n row.bankAmount ??\n 0\n ),\n firstValue(\n row.source_files ||\n row.source_file ||\n ''\n ),\n 'PENDIENTE REVISIÓN',\n '',\n ]\n );\n\nconst diferenciasNombreRows = rows\n .filter(\n (row) =>\n row.category ===\n 'diferencia_nombre_banco'\n )\n .map((row, index) => [\n index + 1,\n normalizeText(\n row.payroll_name ||\n row.employee_name ||\n row.employee ||\n row.bank_name_file ||\n ''\n ),\n normalizeText(\n row.bank_name ||\n row.bank_account_holder ||\n ''\n ),\n normalizeText(\n row.bank_account ||\n row.bankAccount ||\n row.account ||\n ''\n ),\n roundMoney(\n row.bank_amount ??\n row.bankAmount ??\n 0\n ),\n normalizeText(\n row.status ||\n 'Pendiente revisión'\n ).toUpperCase(),\n normalizeText(\n row.observation || ''\n ),\n '',\n ]);\n\nconst cuentaMalDigitadaRows = [];\n\ncuentaMalDigitadaCases.forEach(\n (row, index) => {\n const payrollAccounts =\n payrollAccountsForWrongAccount(row);\n\n const bankAccount =\n bankAccountForWrongAccount(row);\n\n const fields = [\n [\n 'Empleado',\n normalizeText(\n row.employee_name ||\n row.employee ||\n ''\n ),\n ],\n [\n 'Cuentas registradas en las hojas de nómina',\n payrollAccounts.join(' y ') ||\n 'No se identificó una cuenta válida en la nómina.',\n ],\n [\n 'Cuenta utilizada por el banco',\n bankAccount ||\n 'No se identificó una cuenta válida en el banco.',\n ],\n [\n 'Estado del total',\n wrongAccountTotalStatus(row),\n ],\n [\n 'Hallazgo',\n wrongAccountFinding(row),\n ],\n [\n 'Clasificación',\n 'Posible cuenta mal digitada — revisar y unificar la cuenta registrada en nómina.',\n ],\n ];\n\n fields.forEach(\n (field, fieldIndex) => {\n cuentaMalDigitadaRows.push([\n fieldIndex === 0\n ? index + 1\n : '',\n field[0],\n field[1],\n '',\n ]);\n }\n );\n }\n);\n\nconst resumenRows = [\n ['Período', periodLabel],\n [\n 'Coincidencias',\n Number(summary.coincidencias || 0),\n ],\n [\n 'Discrepancias de monto o pago',\n Number(\n summary.discrepanciasMontoPago ??\n summary.discrepancias ??\n 0\n ),\n ],\n [\n 'Banco sin nómina',\n Number(summary.bancoSinNomina || 0),\n ],\n [\n 'Banco sin Bamboo',\n Number(summary.bancoSinBamboo || 0),\n ],\n [\n 'Nómina sin cuenta no conciliada',\n Number(summary.nominaSinCuenta || 0),\n ],\n [\n 'Diferencias de nombre',\n Number(\n summary.diferenciasNombreBanco || 0\n ),\n ],\n [\n 'Posibles cuentas mal digitadas',\n Number(\n summary.posiblesCuentasMalDigitadas || 0\n ),\n ],\n [\n 'Pendientes del cruce principal',\n Number(summary.pendientes || 0),\n ],\n [\n 'Empleados BambooHR Trinidad y Tobago',\n Number(summary.empleadosBambooTT || 0),\n ],\n [\n 'Empleados BambooHR en el período',\n Number(\n summary.empleadosBambooEnPeriodo || 0\n ),\n ],\n [\n 'Filas válidas de nómina',\n Number(\n summary.filasNominaValidas || 0\n ),\n ],\n [\n 'Filas de nómina sin cuenta detectadas',\n Number(\n summary.filasNominaSinCuenta || 0\n ),\n ],\n [\n 'Transacciones bancarias',\n Number(\n summary.transaccionesBanco || 0\n ),\n ],\n [\n 'Total nómina',\n roundMoney(summary.totalNomina || 0),\n ],\n [\n 'Total banco',\n roundMoney(summary.totalBanco || 0),\n ],\n [\n 'Diferencia total',\n roundMoney(\n summary.diferenciaTotal || 0\n ),\n ],\n];\n\nfunction reportValues(\n title,\n subtitle,\n header,\n body\n) {\n return [\n [\n title,\n ...Array(\n Math.max(header.length - 1, 0)\n ).fill(''),\n ],\n [\n subtitle,\n ...Array(\n Math.max(header.length - 1, 0)\n ).fill(''),\n ],\n Array(header.length).fill(''),\n header,\n ...body,\n ];\n}\n\nconst nominaVsBancoValues =\n reportValues(\n 'GOMEZLEE MARKETING',\n `Diferencias de Monto Nómina vs. Banco · Trinidad y Tobago · ${periodEndLabel}`,\n [\n '#',\n 'Empleado',\n 'Cuenta',\n 'Monto en Nómina (TT$)',\n 'Monto en Banco (TT$)',\n 'Diferencia (TT$)',\n 'Sentido',\n 'Estado',\n 'Resolución',\n ],\n mainRows\n );\n\nconst bancoSinNominaValues =\n reportValues(\n 'GOMEZLEE MARKETING',\n `Pagos bancarios sin registro en la nómina · Trinidad y Tobago · ${periodEndLabel}`,\n [\n '#',\n 'Nombre en banco',\n 'Cuenta',\n 'Monto en banco (TT$)',\n 'Archivo',\n 'Estado',\n 'Observación',\n 'Resolución',\n ],\n bancoSinNominaRows\n );\n\nconst bancoSinBambooValues =\n reportValues(\n 'GOMEZLEE MARKETING',\n `Pagos en banco sin empleado identificado en BambooHR · Trinidad y Tobago · ${periodEndLabel}`,\n [\n '#',\n 'Nombre en banco',\n 'Cuenta',\n 'Monto en banco (TT$)',\n 'Archivo',\n 'Estado',\n 'Resolución',\n ],\n bancoSinBambooRows\n );\n\nconst diferenciasNombreValues =\n reportValues(\n 'GOMEZLEE MARKETING',\n `Diferencias de nombre entre nómina y banco · Trinidad y Tobago · ${periodEndLabel}`,\n [\n '#',\n 'Nombre en nómina',\n 'Nombre en banco',\n 'Cuenta',\n 'Monto en banco (TT$)',\n 'Estado',\n 'Observación',\n 'Resolución',\n ],\n diferenciasNombreRows\n );\n\nconst cuentaMalDigitadaValues =\n reportValues(\n 'GOMEZLEE MARKETING',\n `Cuenta Mal Digitada en Nómina · Trinidad y Tobago · ${periodEndLabel}`,\n ['#', 'Campo', 'Detalle', 'Resolución'],\n cuentaMalDigitadaRows\n );\n\nconst resumenValues =\n reportValues(\n 'GOMEZLEE MARKETING',\n `Resumen del cruce Nómina vs. Banco · Trinidad y Tobago · ${periodEndLabel}`,\n ['Indicador', 'Valor'],\n resumenRows\n );\n\nconst valueData = [\n {\n range:\n `'${sheetTitles.nominaVsBanco}'!A1:I`,\n values: nominaVsBancoValues,\n },\n {\n range:\n `'${sheetTitles.bancoSinNomina}'!A1:H`,\n values: bancoSinNominaValues,\n },\n {\n range:\n `'${sheetTitles.bancoSinBamboo}'!A1:G`,\n values: bancoSinBambooValues,\n },\n {\n range:\n `'${sheetTitles.diferenciasNombreBanco}'!A1:H`,\n values: diferenciasNombreValues,\n },\n ...(hasCuentaMalDigitada\n ? [\n {\n range:\n `'${sheetTitles.cuentaMalDigitada}'!A1:D`,\n values:\n cuentaMalDigitadaValues,\n },\n ]\n : []),\n {\n range:\n `'${sheetTitles.resumen}'!A1:B`,\n values: resumenValues,\n },\n];\n\nconst brandColor = {\n red: 0.29,\n green: 0.49,\n blue: 0.58,\n};\n\nconst whiteColor = {\n red: 1,\n green: 1,\n blue: 1,\n};\n\nconst borderColor = {\n red: 0.82,\n green: 0.86,\n blue: 0.88,\n};\n\nfunction mergeRow(\n sheetId,\n rowIndex,\n columnCount\n) {\n return {\n mergeCells: {\n range: {\n sheetId,\n startRowIndex: rowIndex,\n endRowIndex: rowIndex + 1,\n startColumnIndex: 0,\n endColumnIndex: columnCount,\n },\n mergeType: 'MERGE_ALL',\n },\n };\n}\n\nfunction formatRange(\n sheetId,\n startRowIndex,\n endRowIndex,\n startColumnIndex,\n endColumnIndex,\n userEnteredFormat\n) {\n const formatFields =\n Object.keys(userEnteredFormat || {});\n\n return {\n repeatCell: {\n range: {\n sheetId,\n startRowIndex,\n endRowIndex,\n startColumnIndex,\n endColumnIndex,\n },\n cell: {\n userEnteredFormat,\n },\n fields:\n `userEnteredFormat(${formatFields.join(',')})`,\n },\n };\n}\n\nfunction titleFormat(\n sheetId,\n rowIndex,\n columnCount,\n options = {}\n) {\n return formatRange(\n sheetId,\n rowIndex,\n rowIndex + 1,\n 0,\n columnCount,\n {\n backgroundColor: brandColor,\n textFormat: {\n bold: options.bold ?? true,\n italic:\n options.italic ?? false,\n fontSize:\n options.fontSize ?? 12,\n foregroundColor:\n whiteColor,\n },\n horizontalAlignment: 'LEFT',\n verticalAlignment: 'MIDDLE',\n wrapStrategy: 'WRAP',\n }\n );\n}\n\nfunction headerFormat(\n sheetId,\n columnCount\n) {\n return formatRange(\n sheetId,\n 3,\n 4,\n 0,\n columnCount,\n {\n backgroundColor: brandColor,\n textFormat: {\n bold: true,\n foregroundColor:\n whiteColor,\n },\n horizontalAlignment: 'CENTER',\n verticalAlignment: 'MIDDLE',\n wrapStrategy: 'WRAP',\n }\n );\n}\n\nfunction freezeRows(\n sheetId,\n count\n) {\n return {\n updateSheetProperties: {\n properties: {\n sheetId,\n gridProperties: {\n frozenRowCount: count,\n },\n },\n fields:\n 'gridProperties.frozenRowCount',\n },\n };\n}\n\nfunction setFilter(\n sheetId,\n columnCount,\n endRowIndex\n) {\n return {\n setBasicFilter: {\n filter: {\n range: {\n sheetId,\n startRowIndex: 3,\n endRowIndex,\n startColumnIndex: 0,\n endColumnIndex:\n columnCount,\n },\n },\n },\n };\n}\n\nfunction setColumnWidth(\n sheetId,\n index,\n pixelSize\n) {\n return {\n updateDimensionProperties: {\n range: {\n sheetId,\n dimension: 'COLUMNS',\n startIndex: index,\n endIndex: index + 1,\n },\n properties: {\n pixelSize,\n },\n fields: 'pixelSize',\n },\n };\n}\n\nfunction setRowHeight(\n sheetId,\n startIndex,\n endIndex,\n pixelSize\n) {\n return {\n updateDimensionProperties: {\n range: {\n sheetId,\n dimension: 'ROWS',\n startIndex,\n endIndex,\n },\n properties: {\n pixelSize,\n },\n fields: 'pixelSize',\n },\n };\n}\n\nfunction borderFormat(\n sheetId,\n columnCount,\n endRowIndex\n) {\n const border = {\n style: 'SOLID',\n color: borderColor,\n };\n\n return [\n formatRange(\n sheetId,\n 3,\n endRowIndex,\n 0,\n columnCount,\n {\n verticalAlignment: 'MIDDLE',\n wrapStrategy: 'WRAP',\n }\n ),\n {\n updateBorders: {\n range: {\n sheetId,\n startRowIndex: 3,\n endRowIndex,\n startColumnIndex: 0,\n endColumnIndex: columnCount,\n },\n top: border,\n bottom: border,\n left: border,\n right: border,\n innerHorizontal: border,\n innerVertical: border,\n },\n },\n ];\n}\n\nfunction moneyFormat(\n sheetId,\n startColumnIndex,\n endColumnIndex,\n startRowIndex,\n endRowIndex\n) {\n return formatRange(\n sheetId,\n startRowIndex,\n endRowIndex,\n startColumnIndex,\n endColumnIndex,\n {\n numberFormat: {\n type: 'NUMBER',\n pattern:\n '\"TT$\"#,##0.00',\n },\n horizontalAlignment:\n 'RIGHT',\n verticalAlignment:\n 'MIDDLE',\n }\n );\n}\n\nfunction statusFormat(\n sheetId,\n columnIndex,\n endRowIndex\n) {\n return formatRange(\n sheetId,\n 4,\n endRowIndex,\n columnIndex,\n columnIndex + 1,\n {\n backgroundColor: {\n red: 1,\n green: 0.92,\n blue: 0.92,\n },\n textFormat: {\n bold: true,\n foregroundColor: {\n red: 0.82,\n green: 0.08,\n blue: 0.08,\n },\n },\n horizontalAlignment:\n 'CENTER',\n verticalAlignment:\n 'MIDDLE',\n wrapStrategy: 'WRAP',\n }\n );\n}\n\nfunction conditionalDifference(\n sheetId,\n endRowIndex,\n formula,\n backgroundColor,\n textColor\n) {\n return {\n addConditionalFormatRule: {\n rule: {\n ranges: [\n {\n sheetId,\n startRowIndex: 4,\n endRowIndex,\n startColumnIndex: 5,\n endColumnIndex: 6,\n },\n ],\n booleanRule: {\n condition: {\n type: 'CUSTOM_FORMULA',\n values: [\n {\n userEnteredValue:\n formula,\n },\n ],\n },\n format: {\n backgroundColor,\n textFormat: {\n bold: true,\n foregroundColor:\n textColor,\n },\n },\n },\n },\n index: 0,\n },\n };\n}\n\nfunction styleReport(config) {\n const {\n sheetId,\n columnCount,\n bodyRowsCount,\n widths,\n moneyColumns = [],\n statusColumn = null,\n } = config;\n\n const endRowIndex = Math.max(\n 4 + bodyRowsCount,\n 4\n );\n\n const requests = [\n mergeRow(\n sheetId,\n 0,\n columnCount\n ),\n mergeRow(\n sheetId,\n 1,\n columnCount\n ),\n titleFormat(\n sheetId,\n 0,\n columnCount,\n {\n fontSize: 12,\n bold: true,\n }\n ),\n titleFormat(\n sheetId,\n 1,\n columnCount,\n {\n fontSize: 10,\n bold: false,\n italic: true,\n }\n ),\n headerFormat(\n sheetId,\n columnCount\n ),\n freezeRows(sheetId, 4),\n setFilter(\n sheetId,\n columnCount,\n endRowIndex\n ),\n ...borderFormat(\n sheetId,\n columnCount,\n endRowIndex\n ),\n setRowHeight(\n sheetId,\n 0,\n 1,\n 30\n ),\n setRowHeight(\n sheetId,\n 1,\n 2,\n 28\n ),\n setRowHeight(\n sheetId,\n 3,\n 4,\n 42\n ),\n ...widths.map(\n (width, index) =>\n setColumnWidth(\n sheetId,\n index,\n width\n )\n ),\n ];\n\n if (bodyRowsCount > 0) {\n requests.push(\n setRowHeight(\n sheetId,\n 4,\n endRowIndex,\n 30\n )\n );\n\n for (\n const [startColumn, endColumn] of\n moneyColumns\n ) {\n requests.push(\n moneyFormat(\n sheetId,\n startColumn,\n endColumn,\n 4,\n endRowIndex\n )\n );\n }\n\n if (\n Number.isInteger(\n statusColumn\n )\n ) {\n requests.push(\n statusFormat(\n sheetId,\n statusColumn,\n endRowIndex\n )\n );\n }\n }\n\n return requests;\n}\n\nconst formatRequests = [\n ...styleReport({\n sheetId:\n sheetIds.nominaVsBanco,\n columnCount: 9,\n bodyRowsCount:\n mainRows.length,\n widths: [\n 48,\n 250,\n 145,\n 135,\n 135,\n 135,\n 180,\n 120,\n 260,\n ],\n moneyColumns: [\n [3, 6],\n ],\n statusColumn: 7,\n }),\n\n ...(mainRows.length > 0\n ? [\n conditionalDifference(\n sheetIds.nominaVsBanco,\n 4 + mainRows.length,\n '=$F5>0',\n {\n red: 1,\n green: 0.97,\n blue: 0.82,\n },\n {\n red: 0.45,\n green: 0.27,\n blue: 0,\n }\n ),\n conditionalDifference(\n sheetIds.nominaVsBanco,\n 4 + mainRows.length,\n '=$F5<0',\n {\n red: 1,\n green: 0.89,\n blue: 0.89,\n },\n {\n red: 0.85,\n green: 0.05,\n blue: 0.05,\n }\n ),\n ]\n : []),\n\n ...styleReport({\n sheetId:\n sheetIds.bancoSinNomina,\n columnCount: 8,\n bodyRowsCount:\n bancoSinNominaRows.length,\n widths: [\n 48,\n 230,\n 145,\n 135,\n 230,\n 140,\n 360,\n 260,\n ],\n moneyColumns: [[3, 4]],\n statusColumn: 5,\n }),\n\n ...styleReport({\n sheetId:\n sheetIds.bancoSinBamboo,\n columnCount: 7,\n bodyRowsCount:\n bancoSinBambooRows.length,\n widths: [\n 48,\n 250,\n 145,\n 140,\n 250,\n 150,\n 260,\n ],\n moneyColumns: [[3, 4]],\n statusColumn: 5,\n }),\n\n ...styleReport({\n sheetId:\n sheetIds.diferenciasNombreBanco,\n columnCount: 8,\n bodyRowsCount:\n diferenciasNombreRows.length,\n widths: [\n 48,\n 240,\n 240,\n 145,\n 140,\n 150,\n 420,\n 260,\n ],\n moneyColumns: [[4, 5]],\n statusColumn: 5,\n }),\n];\n\nif (hasCuentaMalDigitada) {\n const endRowIndex =\n 4 +\n cuentaMalDigitadaRows.length;\n\n formatRequests.push(\n ...styleReport({\n sheetId:\n sheetIds.cuentaMalDigitada,\n columnCount: 4,\n bodyRowsCount:\n cuentaMalDigitadaRows.length,\n widths: [\n 48,\n 300,\n 520,\n 260,\n ],\n statusColumn: null,\n })\n );\n\n cuentaMalDigitadaCases.forEach(\n (_, caseIndex) => {\n const startRowIndex =\n 4 + caseIndex * 6;\n\n const endCaseRowIndex =\n startRowIndex + 6;\n\n formatRequests.push(\n {\n mergeCells: {\n range: {\n sheetId:\n sheetIds.cuentaMalDigitada,\n startRowIndex,\n endRowIndex:\n endCaseRowIndex,\n startColumnIndex: 0,\n endColumnIndex: 1,\n },\n mergeType:\n 'MERGE_ALL',\n },\n },\n {\n mergeCells: {\n range: {\n sheetId:\n sheetIds.cuentaMalDigitada,\n startRowIndex,\n endRowIndex:\n endCaseRowIndex,\n startColumnIndex: 3,\n endColumnIndex: 4,\n },\n mergeType:\n 'MERGE_ALL',\n },\n },\n formatRange(\n sheetIds.cuentaMalDigitada,\n startRowIndex,\n endCaseRowIndex,\n 0,\n 1,\n {\n backgroundColor: {\n red: 0.91,\n green: 0.95,\n blue: 0.99,\n },\n textFormat: {\n bold: true,\n foregroundColor: {\n red: 0.20,\n green: 0.36,\n blue: 0.45,\n },\n },\n horizontalAlignment:\n 'CENTER',\n verticalAlignment:\n 'MIDDLE',\n wrapStrategy: 'WRAP',\n }\n ),\n formatRange(\n sheetIds.cuentaMalDigitada,\n startRowIndex,\n endCaseRowIndex,\n 1,\n 2,\n {\n backgroundColor: {\n red: 0.93,\n green: 0.97,\n blue: 0.90,\n },\n textFormat: {\n bold: true,\n foregroundColor: {\n red: 0.20,\n green: 0.36,\n blue: 0.45,\n },\n },\n horizontalAlignment:\n 'LEFT',\n verticalAlignment:\n 'MIDDLE',\n wrapStrategy: 'WRAP',\n }\n ),\n formatRange(\n sheetIds.cuentaMalDigitada,\n startRowIndex,\n endCaseRowIndex,\n 2,\n 3,\n {\n horizontalAlignment:\n 'LEFT',\n verticalAlignment:\n 'MIDDLE',\n wrapStrategy: 'WRAP',\n }\n ),\n setRowHeight(\n sheetIds.cuentaMalDigitada,\n startRowIndex,\n endCaseRowIndex,\n 52\n )\n );\n }\n );\n}\n\nconst resumenEndRowIndex =\n 4 + resumenRows.length;\n\nformatRequests.push(\n ...styleReport({\n sheetId:\n sheetIds.resumen,\n columnCount: 2,\n bodyRowsCount:\n resumenRows.length,\n widths: [340, 180],\n statusColumn: null,\n }),\n moneyFormat(\n sheetIds.resumen,\n 1,\n 2,\n resumenEndRowIndex - 3,\n resumenEndRowIndex\n )\n);\n\nreturn [\n {\n json: {\n ok: true,\n stage:\n 'preparar_google_sheet_tt',\n metadata,\n summary,\n spreadsheetTitle,\n sheetIds,\n sheetTitles,\n createSpreadsheetBody: {\n properties: {\n title: spreadsheetTitle,\n },\n sheets: [\n {\n properties: {\n sheetId:\n sheetIds.nominaVsBanco,\n title:\n sheetTitles.nominaVsBanco,\n },\n },\n {\n properties: {\n sheetId:\n sheetIds.bancoSinNomina,\n title:\n sheetTitles.bancoSinNomina,\n },\n },\n {\n properties: {\n sheetId:\n sheetIds.bancoSinBamboo,\n title:\n sheetTitles.bancoSinBamboo,\n },\n },\n {\n properties: {\n sheetId:\n sheetIds.diferenciasNombreBanco,\n title:\n sheetTitles.diferenciasNombreBanco,\n },\n },\n ...(hasCuentaMalDigitada\n ? [\n {\n properties: {\n sheetId:\n sheetIds.cuentaMalDigitada,\n title:\n sheetTitles.cuentaMalDigitada,\n },\n },\n ]\n : []),\n {\n properties: {\n sheetId:\n sheetIds.resumen,\n title:\n sheetTitles.resumen,\n },\n },\n ],\n },\n valueBatchBody: {\n valueInputOption:\n 'RAW',\n data: valueData,\n },\n formatBatchBody: {\n requests: formatRequests,\n },\n originalResponse: data,\n },\n },\n];" + "jsCode": "const data = $input.first().json || {};\n\nfunction normalizeText(value) {\n return String(value ?? '')\n .replace(/\\uFEFF/g, '')\n .replace(/\\u00A0/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim();\n}\n\nfunction roundMoney(value) {\n return Math.round(\n (Number(value) || 0) * 100\n ) / 100;\n}\n\nfunction firstValue(value) {\n if (Array.isArray(value)) {\n return value\n .map(normalizeText)\n .filter(Boolean)\n .join(' / ');\n }\n\n return normalizeText(value);\n}\n\nfunction formatPeriodEnd(value) {\n const raw = normalizeText(value);\n\n if (!/^\\d{4}-\\d{2}-\\d{2}$/.test(raw)) {\n return raw;\n }\n\n const [year, month, day] = raw.split('-');\n\n const monthNames = {\n '01': 'ene',\n '02': 'feb',\n '03': 'mar',\n '04': 'abr',\n '05': 'may',\n '06': 'jun',\n '07': 'jul',\n '08': 'ago',\n '09': 'sep',\n '10': 'oct',\n '11': 'nov',\n '12': 'dic',\n };\n\n return `${day}-${monthNames[month] || month}-${year}`;\n}\n\nfunction mainReportSense(row, difference) {\n const subcategory = normalizeText(\n row.subcategory\n ).toLowerCase();\n\n const payrollAmount = Number(\n row.payroll_amount ??\n row.payrollAmount ??\n 0\n );\n\n const bankAmount = Number(\n row.bank_amount ??\n row.bankAmount ??\n 0\n );\n\n if (\n subcategory ===\n 'nomina_con_cuenta_sin_pago_banco' ||\n (bankAmount === 0 && payrollAmount > 0)\n ) {\n return 'No aparece pagado en banco';\n }\n\n if (difference > 0) {\n return 'Se pagó de menos';\n }\n\n if (difference < 0) {\n return 'Se pagó de más';\n }\n\n return 'Revisar';\n}\n\nfunction accountValues(value) {\n const values = Array.isArray(value)\n ? value\n : String(value ?? '')\n .split(/\\s*(?:\\/|;|,|\\by\\b)\\s*/i);\n\n return values\n .map((item) =>\n String(item ?? '')\n .replace(/\\u00A0/g, '')\n .replace(/\\.0$/g, '')\n .replace(/\\D/g, '')\n .trim()\n )\n .filter(\n (account) =>\n account.length >= 6 &&\n !/^0+$/.test(account)\n );\n}\n\nfunction payrollAccountsForWrongAccount(row) {\n const candidates = [\n row.payroll_account,\n row.payrollAccount,\n ...(Array.isArray(row.source_rows)\n ? row.source_rows.flatMap(\n (sourceRow) => [\n sourceRow.account,\n sourceRow.payroll_account,\n sourceRow.payrollAccount,\n ]\n )\n : []),\n ];\n\n return Array.from(\n new Set(\n candidates.flatMap(accountValues)\n )\n );\n}\n\nfunction bankAccountForWrongAccount(row) {\n return firstValue(\n row.bank_account ||\n row.bankAccount ||\n row.account ||\n ''\n );\n}\n\nfunction moneyLabel(value) {\n return Math.abs(\n roundMoney(value)\n ).toLocaleString('en-US', {\n minimumFractionDigits: 2,\n maximumFractionDigits: 2,\n });\n}\n\nfunction wrongAccountTotalStatus(row) {\n const payrollAmount = roundMoney(\n row.payroll_amount ??\n row.payrollAmount ??\n 0\n );\n\n const bankAmount = roundMoney(\n row.bank_amount ??\n row.bankAmount ??\n 0\n );\n\n const difference = roundMoney(\n payrollAmount - bankAmount\n );\n\n if (Math.abs(difference) <= 0.02) {\n return (\n 'El total de nómina coincide con ' +\n 'el total pagado por el banco.'\n );\n }\n\n if (difference > 0) {\n return (\n 'El total de nómina supera el total ' +\n `del banco por TT$${moneyLabel(difference)}.`\n );\n }\n\n return (\n 'El total pagado por el banco supera ' +\n `el total de nómina por TT$${moneyLabel(difference)}.`\n );\n}\n\nfunction wrongAccountFinding(row) {\n const existing = normalizeText(\n row.observation || ''\n );\n\n if (existing) return existing;\n\n const payrollAccounts =\n payrollAccountsForWrongAccount(row);\n\n const bankAccount =\n bankAccountForWrongAccount(row);\n\n return (\n 'El empleado presenta una posible ' +\n 'inconsistencia entre la cuenta registrada ' +\n `en nómina (${payrollAccounts.join(' y ') || 'sin cuenta identificada'}) ` +\n `y la cuenta utilizada por el banco (${bankAccount || 'sin cuenta identificada'}).`\n );\n}\n\nconst metadata = data.metadata || {};\nconst summary = data.summary || {};\n\nconst rows = Array.isArray(data.rows)\n ? data.rows\n : [];\n\nconst bankWithoutBamboo =\n Array.isArray(data.bankWithoutBamboo)\n ? data.bankWithoutBamboo\n : [];\n\nconst periodLabel =\n metadata.period_label ||\n `${metadata.year || ''}-${metadata.month || ''}-${metadata.period_type || ''}`;\n\nconst periodEndLabel = formatPeriodEnd(\n metadata.period_end || ''\n);\n\nconst spreadsheetTitle =\n `Cruce de Cuentas GLM TT - ${periodLabel}`;\n\nconst sheetIds = {\n nominaVsBanco: 201,\n bancoSinNomina: 202,\n bancoSinBamboo: 203,\n diferenciasNombreBanco: 204,\n cuentaMalDigitada: 205,\n resumen: 206,\n};\n\nconst cuentaMalDigitadaCases = rows.filter(\n (row) =>\n row.category ===\n 'posible_cuenta_mal_digitada'\n);\n\nconst hasCuentaMalDigitada =\n cuentaMalDigitadaCases.length > 0;\n\nconst sheetTitles = {\n nominaVsBanco:\n '01 Nómina vs Banco',\n bancoSinNomina:\n '02 Banco sin Nómina',\n bancoSinBamboo:\n '03 Banco sin Bamboo',\n diferenciasNombreBanco:\n '04 Diferencias de Nombre',\n cuentaMalDigitada:\n '05 Cuenta Mal Digitada',\n resumen: hasCuentaMalDigitada\n ? '06 Resumen'\n : '05 Resumen',\n};\n\nconst mainRows = rows\n .filter(\n (row) =>\n row.category === 'discrepancia'\n )\n .map((row, index) => {\n const payrollAmount = roundMoney(\n row.payroll_amount ??\n row.payrollAmount ??\n 0\n );\n\n const bankAmount = roundMoney(\n row.bank_amount ??\n row.bankAmount ??\n 0\n );\n\n const difference = roundMoney(\n row.difference ??\n (payrollAmount - bankAmount)\n );\n\n return [\n index + 1,\n normalizeText(\n row.employee_name ||\n row.employee ||\n ''\n ),\n normalizeText(\n row.bank_account ||\n row.bankAccount ||\n row.payroll_account ||\n row.payrollAccount ||\n row.account ||\n ''\n ),\n payrollAmount,\n bankAmount,\n difference,\n mainReportSense(\n row,\n difference\n ),\n normalizeText(\n row.status || 'Riesgo'\n ).toUpperCase(),\n '',\n ];\n });\n\nconst bancoSinNominaRows = rows\n .filter(\n (row) =>\n row.category ===\n 'banco_sin_nomina'\n )\n .map((row, index) => [\n index + 1,\n normalizeText(\n row.employee_name ||\n row.employee ||\n ''\n ),\n normalizeText(\n row.bank_account ||\n row.bankAccount ||\n row.account ||\n ''\n ),\n roundMoney(\n row.bank_amount ??\n row.bankAmount ??\n 0\n ),\n firstValue(\n row.source_files ||\n row.source_file ||\n ''\n ),\n normalizeText(\n row.status ||\n 'Pendiente revisión'\n ).toUpperCase(),\n normalizeText(\n row.observation || ''\n ),\n '',\n ]);\n\nconst bancoSinBambooRows =\n bankWithoutBamboo.map(\n (row, index) => [\n index + 1,\n normalizeText(\n row.bank_name_file ||\n row.employee_name ||\n row.employee ||\n ''\n ),\n normalizeText(\n row.bank_account ||\n row.bankAccount ||\n row.account ||\n ''\n ),\n roundMoney(\n row.bank_amount ??\n row.bankAmount ??\n 0\n ),\n firstValue(\n row.source_files ||\n row.source_file ||\n ''\n ),\n 'PENDIENTE REVISIÓN',\n '',\n ]\n );\n\nconst diferenciasNombreRows = rows\n .filter(\n (row) =>\n row.category ===\n 'diferencia_nombre_banco'\n )\n .map((row, index) => [\n index + 1,\n normalizeText(\n row.payroll_name ||\n row.employee_name ||\n row.employee ||\n row.bank_name_file ||\n ''\n ),\n normalizeText(\n row.bank_name ||\n row.bank_account_holder ||\n ''\n ),\n normalizeText(\n row.bank_account ||\n row.bankAccount ||\n row.account ||\n ''\n ),\n roundMoney(\n row.bank_amount ??\n row.bankAmount ??\n 0\n ),\n normalizeText(\n row.status ||\n 'Pendiente revisión'\n ).toUpperCase(),\n normalizeText(\n row.observation || ''\n ),\n '',\n ]);\n\nconst cuentaMalDigitadaRows = [];\n\ncuentaMalDigitadaCases.forEach(\n (row, index) => {\n const payrollAccounts =\n payrollAccountsForWrongAccount(row);\n\n const bankAccount =\n bankAccountForWrongAccount(row);\n\n const fields = [\n [\n 'Empleado',\n normalizeText(\n row.employee_name ||\n row.employee ||\n ''\n ),\n ],\n [\n 'Cuentas registradas en las hojas de nómina',\n payrollAccounts.join(' y ') ||\n 'No se identificó una cuenta válida en la nómina.',\n ],\n [\n 'Cuenta utilizada por el banco',\n bankAccount ||\n 'No se identificó una cuenta válida en el banco.',\n ],\n [\n 'Estado del total',\n wrongAccountTotalStatus(row),\n ],\n [\n 'Hallazgo',\n wrongAccountFinding(row),\n ],\n [\n 'Clasificación',\n 'Posible cuenta mal digitada — revisar y unificar la cuenta registrada en nómina.',\n ],\n ];\n\n fields.forEach(\n (field, fieldIndex) => {\n cuentaMalDigitadaRows.push([\n fieldIndex === 0\n ? index + 1\n : '',\n field[0],\n field[1],\n '',\n ]);\n }\n );\n }\n);\n\nconst resumenRows = [\n ['Período', periodLabel],\n [\n 'Coincidencias',\n Number(summary.coincidencias || 0),\n ],\n [\n 'Discrepancias de monto o pago',\n Number(\n summary.discrepanciasMontoPago ??\n summary.discrepancias ??\n 0\n ),\n ],\n [\n 'Banco sin nómina',\n Number(summary.bancoSinNomina || 0),\n ],\n [\n 'Banco sin Bamboo',\n Number(summary.bancoSinBamboo || 0),\n ],\n [\n 'Nómina sin cuenta no conciliada',\n Number(summary.nominaSinCuenta || 0),\n ],\n [\n 'Diferencias de nombre',\n Number(\n summary.diferenciasNombreBanco || 0\n ),\n ],\n [\n 'Posibles cuentas mal digitadas',\n Number(\n summary.posiblesCuentasMalDigitadas || 0\n ),\n ],\n [\n 'Pendientes del cruce principal',\n Number(summary.pendientes || 0),\n ],\n [\n 'Empleados BambooHR Trinidad y Tobago',\n Number(summary.empleadosBambooTT || 0),\n ],\n [\n 'Empleados BambooHR en el período',\n Number(\n summary.empleadosBambooEnPeriodo || 0\n ),\n ],\n [\n 'Filas válidas de nómina',\n Number(\n summary.filasNominaValidas || 0\n ),\n ],\n [\n 'Filas de nómina sin cuenta detectadas',\n Number(\n summary.filasNominaSinCuenta || 0\n ),\n ],\n [\n 'Transacciones bancarias',\n Number(\n summary.transaccionesBanco || 0\n ),\n ],\n [\n 'Total nómina',\n roundMoney(summary.totalNomina || 0),\n ],\n [\n 'Total banco',\n roundMoney(summary.totalBanco || 0),\n ],\n [\n 'Diferencia total',\n roundMoney(\n summary.diferenciaTotal || 0\n ),\n ],\n];\n\nfunction reportValues(\n title,\n subtitle,\n header,\n body\n) {\n return [\n [\n title,\n ...Array(\n Math.max(header.length - 1, 0)\n ).fill(''),\n ],\n [\n subtitle,\n ...Array(\n Math.max(header.length - 1, 0)\n ).fill(''),\n ],\n Array(header.length).fill(''),\n header,\n ...body,\n ];\n}\n\nconst nominaVsBancoValues =\n reportValues(\n 'GOMEZLEE MARKETING',\n `Diferencias de Monto Nómina vs. Banco · Trinidad y Tobago · ${periodEndLabel}`,\n [\n '#',\n 'Empleado',\n 'Cuenta',\n 'Monto en Nómina (TT$)',\n 'Monto en Banco (TT$)',\n 'Diferencia (TT$)',\n 'Sentido',\n 'Estado',\n 'Resolución',\n ],\n mainRows\n );\n\nconst bancoSinNominaValues =\n reportValues(\n 'GOMEZLEE MARKETING',\n `Pagos bancarios sin registro en la nómina · Trinidad y Tobago · ${periodEndLabel}`,\n [\n '#',\n 'Nombre en banco',\n 'Cuenta',\n 'Monto en banco (TT$)',\n 'Archivo',\n 'Estado',\n 'Observación',\n 'Resolución',\n ],\n bancoSinNominaRows\n );\n\nconst bancoSinBambooValues =\n reportValues(\n 'GOMEZLEE MARKETING',\n `Pagos en banco sin empleado identificado en BambooHR · Trinidad y Tobago · ${periodEndLabel}`,\n [\n '#',\n 'Nombre en banco',\n 'Cuenta',\n 'Monto en banco (TT$)',\n 'Archivo',\n 'Estado',\n 'Resolución',\n ],\n bancoSinBambooRows\n );\n\nconst diferenciasNombreValues =\n reportValues(\n 'GOMEZLEE MARKETING',\n `Diferencias de nombre entre nómina y banco · Trinidad y Tobago · ${periodEndLabel}`,\n [\n '#',\n 'Nombre en nómina',\n 'Nombre en banco',\n 'Cuenta',\n 'Monto en banco (TT$)',\n 'Estado',\n 'Observación',\n 'Resolución',\n ],\n diferenciasNombreRows\n );\n\nconst cuentaMalDigitadaValues =\n reportValues(\n 'GOMEZLEE MARKETING',\n `Cuenta Mal Digitada en Nómina · Trinidad y Tobago · ${periodEndLabel}`,\n ['#', 'Campo', 'Detalle', 'Resolución'],\n cuentaMalDigitadaRows\n );\n\nconst resumenValues =\n reportValues(\n 'GOMEZLEE MARKETING',\n `Resumen del cruce Nómina vs. Banco · Trinidad y Tobago · ${periodEndLabel}`,\n ['Indicador', 'Valor'],\n resumenRows\n );\n\nconst valueData = [\n {\n range:\n `'${sheetTitles.nominaVsBanco}'!A1:I`,\n values: nominaVsBancoValues,\n },\n {\n range:\n `'${sheetTitles.bancoSinNomina}'!A1:H`,\n values: bancoSinNominaValues,\n },\n {\n range:\n `'${sheetTitles.bancoSinBamboo}'!A1:G`,\n values: bancoSinBambooValues,\n },\n {\n range:\n `'${sheetTitles.diferenciasNombreBanco}'!A1:H`,\n values: diferenciasNombreValues,\n },\n ...(hasCuentaMalDigitada\n ? [\n {\n range:\n `'${sheetTitles.cuentaMalDigitada}'!A1:D`,\n values:\n cuentaMalDigitadaValues,\n },\n ]\n : []),\n {\n range:\n `'${sheetTitles.resumen}'!A1:B`,\n values: resumenValues,\n },\n];\n\nconst brandColor = {\n red: 0.29,\n green: 0.49,\n blue: 0.58,\n};\n\nconst whiteColor = {\n red: 1,\n green: 1,\n blue: 1,\n};\n\nconst borderColor = {\n red: 0.82,\n green: 0.86,\n blue: 0.88,\n};\n\nfunction mergeRow(\n sheetId,\n rowIndex,\n columnCount\n) {\n return {\n mergeCells: {\n range: {\n sheetId,\n startRowIndex: rowIndex,\n endRowIndex: rowIndex + 1,\n startColumnIndex: 0,\n endColumnIndex: columnCount,\n },\n mergeType: 'MERGE_ALL',\n },\n };\n}\n\nfunction formatRange(\n sheetId,\n startRowIndex,\n endRowIndex,\n startColumnIndex,\n endColumnIndex,\n userEnteredFormat\n) {\n const formatFields =\n Object.keys(userEnteredFormat || {});\n\n return {\n repeatCell: {\n range: {\n sheetId,\n startRowIndex,\n endRowIndex,\n startColumnIndex,\n endColumnIndex,\n },\n cell: {\n userEnteredFormat,\n },\n fields:\n `userEnteredFormat(${formatFields.join(',')})`,\n },\n };\n}\n\nfunction titleFormat(\n sheetId,\n rowIndex,\n columnCount,\n options = {}\n) {\n return formatRange(\n sheetId,\n rowIndex,\n rowIndex + 1,\n 0,\n columnCount,\n {\n backgroundColor: brandColor,\n textFormat: {\n bold: options.bold ?? true,\n italic:\n options.italic ?? false,\n fontSize:\n options.fontSize ?? 12,\n foregroundColor:\n whiteColor,\n },\n horizontalAlignment: 'LEFT',\n verticalAlignment: 'MIDDLE',\n wrapStrategy: 'WRAP',\n }\n );\n}\n\nfunction headerFormat(\n sheetId,\n columnCount\n) {\n return formatRange(\n sheetId,\n 3,\n 4,\n 0,\n columnCount,\n {\n backgroundColor: brandColor,\n textFormat: {\n bold: true,\n foregroundColor:\n whiteColor,\n },\n horizontalAlignment: 'CENTER',\n verticalAlignment: 'MIDDLE',\n wrapStrategy: 'WRAP',\n }\n );\n}\n\nfunction freezeRows(\n sheetId,\n count\n) {\n return {\n updateSheetProperties: {\n properties: {\n sheetId,\n gridProperties: {\n frozenRowCount: count,\n },\n },\n fields:\n 'gridProperties.frozenRowCount',\n },\n };\n}\n\nfunction setFilter(\n sheetId,\n columnCount,\n endRowIndex\n) {\n return {\n setBasicFilter: {\n filter: {\n range: {\n sheetId,\n startRowIndex: 3,\n endRowIndex,\n startColumnIndex: 0,\n endColumnIndex:\n columnCount,\n },\n },\n },\n };\n}\n\nfunction setColumnWidth(\n sheetId,\n index,\n pixelSize\n) {\n return {\n updateDimensionProperties: {\n range: {\n sheetId,\n dimension: 'COLUMNS',\n startIndex: index,\n endIndex: index + 1,\n },\n properties: {\n pixelSize,\n },\n fields: 'pixelSize',\n },\n };\n}\n\nfunction setRowHeight(\n sheetId,\n startIndex,\n endIndex,\n pixelSize\n) {\n return {\n updateDimensionProperties: {\n range: {\n sheetId,\n dimension: 'ROWS',\n startIndex,\n endIndex,\n },\n properties: {\n pixelSize,\n },\n fields: 'pixelSize',\n },\n };\n}\n\nfunction borderFormat(\n sheetId,\n columnCount,\n endRowIndex\n) {\n const border = {\n style: 'SOLID',\n color: borderColor,\n };\n\n return [\n formatRange(\n sheetId,\n 3,\n endRowIndex,\n 0,\n columnCount,\n {\n verticalAlignment: 'MIDDLE',\n wrapStrategy: 'WRAP',\n }\n ),\n {\n updateBorders: {\n range: {\n sheetId,\n startRowIndex: 3,\n endRowIndex,\n startColumnIndex: 0,\n endColumnIndex: columnCount,\n },\n top: border,\n bottom: border,\n left: border,\n right: border,\n innerHorizontal: border,\n innerVertical: border,\n },\n },\n ];\n}\n\nfunction moneyFormat(\n sheetId,\n startColumnIndex,\n endColumnIndex,\n startRowIndex,\n endRowIndex\n) {\n return formatRange(\n sheetId,\n startRowIndex,\n endRowIndex,\n startColumnIndex,\n endColumnIndex,\n {\n numberFormat: {\n type: 'NUMBER',\n pattern:\n '\"TT$\"#,##0.00',\n },\n horizontalAlignment:\n 'RIGHT',\n verticalAlignment:\n 'MIDDLE',\n }\n );\n}\n\nfunction statusFormat(\n sheetId,\n columnIndex,\n endRowIndex\n) {\n return formatRange(\n sheetId,\n 4,\n endRowIndex,\n columnIndex,\n columnIndex + 1,\n {\n backgroundColor: {\n red: 1,\n green: 0.92,\n blue: 0.92,\n },\n textFormat: {\n bold: true,\n foregroundColor: {\n red: 0.82,\n green: 0.08,\n blue: 0.08,\n },\n },\n horizontalAlignment:\n 'CENTER',\n verticalAlignment:\n 'MIDDLE',\n wrapStrategy: 'WRAP',\n }\n );\n}\n\nfunction conditionalDifference(\n sheetId,\n endRowIndex,\n formula,\n backgroundColor,\n textColor\n) {\n return {\n addConditionalFormatRule: {\n rule: {\n ranges: [\n {\n sheetId,\n startRowIndex: 4,\n endRowIndex,\n startColumnIndex: 5,\n endColumnIndex: 6,\n },\n ],\n booleanRule: {\n condition: {\n type: 'CUSTOM_FORMULA',\n values: [\n {\n userEnteredValue:\n formula,\n },\n ],\n },\n format: {\n backgroundColor,\n textFormat: {\n bold: true,\n foregroundColor:\n textColor,\n },\n },\n },\n },\n index: 0,\n },\n };\n}\n\nfunction styleReport(config) {\n const {\n sheetId,\n columnCount,\n bodyRowsCount,\n widths,\n moneyColumns = [],\n statusColumn = null,\n } = config;\n\n const endRowIndex = Math.max(\n 4 + bodyRowsCount,\n 4\n );\n\n const requests = [\n mergeRow(\n sheetId,\n 0,\n columnCount\n ),\n mergeRow(\n sheetId,\n 1,\n columnCount\n ),\n titleFormat(\n sheetId,\n 0,\n columnCount,\n {\n fontSize: 12,\n bold: true,\n }\n ),\n titleFormat(\n sheetId,\n 1,\n columnCount,\n {\n fontSize: 10,\n bold: false,\n italic: true,\n }\n ),\n headerFormat(\n sheetId,\n columnCount\n ),\n freezeRows(sheetId, 4),\n setFilter(\n sheetId,\n columnCount,\n endRowIndex\n ),\n ...borderFormat(\n sheetId,\n columnCount,\n endRowIndex\n ),\n setRowHeight(\n sheetId,\n 0,\n 1,\n 30\n ),\n setRowHeight(\n sheetId,\n 1,\n 2,\n 28\n ),\n setRowHeight(\n sheetId,\n 3,\n 4,\n 42\n ),\n ...widths.map(\n (width, index) =>\n setColumnWidth(\n sheetId,\n index,\n width\n )\n ),\n ];\n\n if (bodyRowsCount > 0) {\n requests.push(\n setRowHeight(\n sheetId,\n 4,\n endRowIndex,\n 30\n )\n );\n\n for (\n const [startColumn, endColumn] of\n moneyColumns\n ) {\n requests.push(\n moneyFormat(\n sheetId,\n startColumn,\n endColumn,\n 4,\n endRowIndex\n )\n );\n }\n\n if (\n Number.isInteger(\n statusColumn\n )\n ) {\n requests.push(\n statusFormat(\n sheetId,\n statusColumn,\n endRowIndex\n )\n );\n }\n }\n\n return requests;\n}\n\n\nfunction wrapRangeRequest(\n sheetId,\n startRowIndex,\n endRowIndex,\n startColumnIndex,\n endColumnIndex\n) {\n return {\n repeatCell: {\n range: {\n sheetId,\n startRowIndex,\n endRowIndex,\n startColumnIndex,\n endColumnIndex,\n },\n cell: {\n userEnteredFormat: {\n verticalAlignment: 'MIDDLE',\n wrapStrategy: 'WRAP',\n },\n },\n fields:\n 'userEnteredFormat(verticalAlignment,wrapStrategy)',\n },\n };\n}\n\nfunction autoResizeRowsRequest(\n sheetId,\n startIndex,\n endIndex\n) {\n return {\n autoResizeDimensions: {\n dimensions: {\n sheetId,\n dimension: 'ROWS',\n startIndex,\n endIndex,\n },\n },\n };\n}\n\nconst formatRequests = [\n ...styleReport({\n sheetId:\n sheetIds.nominaVsBanco,\n columnCount: 9,\n bodyRowsCount:\n mainRows.length,\n widths: [\n 48,\n 250,\n 145,\n 135,\n 135,\n 135,\n 180,\n 120,\n 260,\n ],\n moneyColumns: [\n [3, 6],\n ],\n statusColumn: 7,\n }),\n\n ...(mainRows.length > 0\n ? [\n conditionalDifference(\n sheetIds.nominaVsBanco,\n 4 + mainRows.length,\n '=$F5>0',\n {\n red: 1,\n green: 0.97,\n blue: 0.82,\n },\n {\n red: 0.45,\n green: 0.27,\n blue: 0,\n }\n ),\n conditionalDifference(\n sheetIds.nominaVsBanco,\n 4 + mainRows.length,\n '=$F5<0',\n {\n red: 1,\n green: 0.89,\n blue: 0.89,\n },\n {\n red: 0.85,\n green: 0.05,\n blue: 0.05,\n }\n ),\n ]\n : []),\n\n ...styleReport({\n sheetId:\n sheetIds.bancoSinNomina,\n columnCount: 8,\n bodyRowsCount:\n bancoSinNominaRows.length,\n widths: [\n 48,\n 230,\n 145,\n 135,\n 230,\n 140,\n 360,\n 260,\n ],\n moneyColumns: [[3, 4]],\n statusColumn: 5,\n }),\n\n ...styleReport({\n sheetId:\n sheetIds.bancoSinBamboo,\n columnCount: 7,\n bodyRowsCount:\n bancoSinBambooRows.length,\n widths: [\n 48,\n 250,\n 145,\n 140,\n 250,\n 150,\n 260,\n ],\n moneyColumns: [[3, 4]],\n statusColumn: 5,\n }),\n\n ...styleReport({\n sheetId:\n sheetIds.diferenciasNombreBanco,\n columnCount: 8,\n bodyRowsCount:\n diferenciasNombreRows.length,\n widths: [\n 48,\n 240,\n 240,\n 145,\n 140,\n 150,\n 420,\n 260,\n ],\n moneyColumns: [[4, 5]],\n statusColumn: 5,\n }),\n];\n\nif (hasCuentaMalDigitada) {\n const endRowIndex =\n 4 +\n cuentaMalDigitadaRows.length;\n\n formatRequests.push(\n ...styleReport({\n sheetId:\n sheetIds.cuentaMalDigitada,\n columnCount: 4,\n bodyRowsCount:\n cuentaMalDigitadaRows.length,\n widths: [\n 48,\n 300,\n 520,\n 260,\n ],\n statusColumn: null,\n })\n );\n\n cuentaMalDigitadaCases.forEach(\n (_, caseIndex) => {\n const startRowIndex =\n 4 + caseIndex * 6;\n\n const endCaseRowIndex =\n startRowIndex + 6;\n\n formatRequests.push(\n {\n mergeCells: {\n range: {\n sheetId:\n sheetIds.cuentaMalDigitada,\n startRowIndex,\n endRowIndex:\n endCaseRowIndex,\n startColumnIndex: 0,\n endColumnIndex: 1,\n },\n mergeType:\n 'MERGE_ALL',\n },\n },\n {\n mergeCells: {\n range: {\n sheetId:\n sheetIds.cuentaMalDigitada,\n startRowIndex,\n endRowIndex:\n endCaseRowIndex,\n startColumnIndex: 3,\n endColumnIndex: 4,\n },\n mergeType:\n 'MERGE_ALL',\n },\n },\n formatRange(\n sheetIds.cuentaMalDigitada,\n startRowIndex,\n endCaseRowIndex,\n 0,\n 1,\n {\n backgroundColor: {\n red: 0.91,\n green: 0.95,\n blue: 0.99,\n },\n textFormat: {\n bold: true,\n foregroundColor: {\n red: 0.20,\n green: 0.36,\n blue: 0.45,\n },\n },\n horizontalAlignment:\n 'CENTER',\n verticalAlignment:\n 'MIDDLE',\n wrapStrategy: 'WRAP',\n }\n ),\n formatRange(\n sheetIds.cuentaMalDigitada,\n startRowIndex,\n endCaseRowIndex,\n 1,\n 2,\n {\n backgroundColor: {\n red: 0.93,\n green: 0.97,\n blue: 0.90,\n },\n textFormat: {\n bold: true,\n foregroundColor: {\n red: 0.20,\n green: 0.36,\n blue: 0.45,\n },\n },\n horizontalAlignment:\n 'LEFT',\n verticalAlignment:\n 'MIDDLE',\n wrapStrategy: 'WRAP',\n }\n ),\n formatRange(\n sheetIds.cuentaMalDigitada,\n startRowIndex,\n endCaseRowIndex,\n 2,\n 3,\n {\n horizontalAlignment:\n 'LEFT',\n verticalAlignment:\n 'MIDDLE',\n wrapStrategy: 'WRAP',\n }\n ),\n setRowHeight(\n sheetIds.cuentaMalDigitada,\n startRowIndex,\n endCaseRowIndex,\n 52\n )\n );\n }\n );\n}\n\nconst resumenEndRowIndex =\n 4 + resumenRows.length;\n\nformatRequests.push(\n ...styleReport({\n sheetId:\n sheetIds.resumen,\n columnCount: 2,\n bodyRowsCount:\n resumenRows.length,\n widths: [340, 180],\n statusColumn: null,\n }),\n moneyFormat(\n sheetIds.resumen,\n 1,\n 2,\n resumenEndRowIndex - 3,\n resumenEndRowIndex\n )\n);\n\n\nconst mainReadabilityEndRow =\n 4 + mainRows.length;\n\nconst bancoSinNominaReadabilityEndRow =\n 4 + bancoSinNominaRows.length;\n\nconst bancoSinBambooReadabilityEndRow =\n 4 + bancoSinBambooRows.length;\n\nconst diferenciasNombreReadabilityEndRow =\n 4 + diferenciasNombreRows.length;\n\nconst resumenReadabilityEndRow =\n 4 + resumenRows.length;\n\n/*\n * Ajuste final de legibilidad.\n *\n * Los anchos definitivos se aplican antes del autoajuste vertical. De esta\n * forma Google Sheets calcula la altura real de cada fila después de envolver\n * el texto, evitando contenido cortado en nombres, observaciones y resúmenes.\n */\nformatRequests.push(\n // 01 Nómina vs Banco\n setColumnWidth(\n sheetIds.nominaVsBanco,\n 1,\n 300\n ),\n setColumnWidth(\n sheetIds.nominaVsBanco,\n 2,\n 150\n ),\n setColumnWidth(\n sheetIds.nominaVsBanco,\n 3,\n 145\n ),\n setColumnWidth(\n sheetIds.nominaVsBanco,\n 4,\n 145\n ),\n setColumnWidth(\n sheetIds.nominaVsBanco,\n 5,\n 145\n ),\n setColumnWidth(\n sheetIds.nominaVsBanco,\n 6,\n 220\n ),\n setColumnWidth(\n sheetIds.nominaVsBanco,\n 7,\n 140\n ),\n setColumnWidth(\n sheetIds.nominaVsBanco,\n 8,\n 320\n ),\n ...(mainRows.length > 0\n ? [\n wrapRangeRequest(\n sheetIds.nominaVsBanco,\n 4,\n mainReadabilityEndRow,\n 0,\n 9\n ),\n autoResizeRowsRequest(\n sheetIds.nominaVsBanco,\n 4,\n mainReadabilityEndRow\n ),\n ]\n : []),\n\n // 02 Banco sin Nómina\n setColumnWidth(\n sheetIds.bancoSinNomina,\n 1,\n 320\n ),\n setColumnWidth(\n sheetIds.bancoSinNomina,\n 2,\n 160\n ),\n setColumnWidth(\n sheetIds.bancoSinNomina,\n 3,\n 145\n ),\n setColumnWidth(\n sheetIds.bancoSinNomina,\n 4,\n 260\n ),\n setColumnWidth(\n sheetIds.bancoSinNomina,\n 5,\n 170\n ),\n setColumnWidth(\n sheetIds.bancoSinNomina,\n 6,\n 560\n ),\n setColumnWidth(\n sheetIds.bancoSinNomina,\n 7,\n 320\n ),\n ...(bancoSinNominaRows.length > 0\n ? [\n wrapRangeRequest(\n sheetIds.bancoSinNomina,\n 4,\n bancoSinNominaReadabilityEndRow,\n 0,\n 8\n ),\n autoResizeRowsRequest(\n sheetIds.bancoSinNomina,\n 4,\n bancoSinNominaReadabilityEndRow\n ),\n ]\n : []),\n\n // 03 Banco sin Bamboo\n setColumnWidth(\n sheetIds.bancoSinBamboo,\n 1,\n 320\n ),\n setColumnWidth(\n sheetIds.bancoSinBamboo,\n 2,\n 160\n ),\n setColumnWidth(\n sheetIds.bancoSinBamboo,\n 3,\n 145\n ),\n setColumnWidth(\n sheetIds.bancoSinBamboo,\n 4,\n 260\n ),\n setColumnWidth(\n sheetIds.bancoSinBamboo,\n 5,\n 170\n ),\n setColumnWidth(\n sheetIds.bancoSinBamboo,\n 6,\n 320\n ),\n ...(bancoSinBambooRows.length > 0\n ? [\n wrapRangeRequest(\n sheetIds.bancoSinBamboo,\n 4,\n bancoSinBambooReadabilityEndRow,\n 0,\n 7\n ),\n autoResizeRowsRequest(\n sheetIds.bancoSinBamboo,\n 4,\n bancoSinBambooReadabilityEndRow\n ),\n ]\n : []),\n\n // 04 Diferencias de Nombre\n setColumnWidth(\n sheetIds.diferenciasNombreBanco,\n 1,\n 320\n ),\n setColumnWidth(\n sheetIds.diferenciasNombreBanco,\n 2,\n 320\n ),\n setColumnWidth(\n sheetIds.diferenciasNombreBanco,\n 3,\n 160\n ),\n setColumnWidth(\n sheetIds.diferenciasNombreBanco,\n 4,\n 145\n ),\n setColumnWidth(\n sheetIds.diferenciasNombreBanco,\n 5,\n 170\n ),\n setColumnWidth(\n sheetIds.diferenciasNombreBanco,\n 6,\n 600\n ),\n setColumnWidth(\n sheetIds.diferenciasNombreBanco,\n 7,\n 320\n ),\n ...(diferenciasNombreRows.length > 0\n ? [\n wrapRangeRequest(\n sheetIds.diferenciasNombreBanco,\n 4,\n diferenciasNombreReadabilityEndRow,\n 0,\n 8\n ),\n autoResizeRowsRequest(\n sheetIds.diferenciasNombreBanco,\n 4,\n diferenciasNombreReadabilityEndRow\n ),\n ]\n : []),\n\n // 05 Cuenta Mal Digitada\n ...(hasCuentaMalDigitada\n ? [\n setColumnWidth(\n sheetIds.cuentaMalDigitada,\n 1,\n 300\n ),\n setColumnWidth(\n sheetIds.cuentaMalDigitada,\n 2,\n 600\n ),\n setColumnWidth(\n sheetIds.cuentaMalDigitada,\n 3,\n 320\n ),\n ]\n : []),\n\n // Resumen\n setColumnWidth(\n sheetIds.resumen,\n 0,\n 380\n ),\n setColumnWidth(\n sheetIds.resumen,\n 1,\n 320\n ),\n ...(resumenRows.length > 0\n ? [\n wrapRangeRequest(\n sheetIds.resumen,\n 4,\n resumenReadabilityEndRow,\n 0,\n 2\n ),\n autoResizeRowsRequest(\n sheetIds.resumen,\n 4,\n resumenReadabilityEndRow\n ),\n ]\n : [])\n);\n\n\nreturn [\n {\n json: {\n ok: true,\n stage:\n 'preparar_google_sheet_tt',\n metadata,\n summary,\n spreadsheetTitle,\n sheetIds,\n sheetTitles,\n createSpreadsheetBody: {\n properties: {\n title: spreadsheetTitle,\n },\n sheets: [\n {\n properties: {\n sheetId:\n sheetIds.nominaVsBanco,\n title:\n sheetTitles.nominaVsBanco,\n },\n },\n {\n properties: {\n sheetId:\n sheetIds.bancoSinNomina,\n title:\n sheetTitles.bancoSinNomina,\n },\n },\n {\n properties: {\n sheetId:\n sheetIds.bancoSinBamboo,\n title:\n sheetTitles.bancoSinBamboo,\n },\n },\n {\n properties: {\n sheetId:\n sheetIds.diferenciasNombreBanco,\n title:\n sheetTitles.diferenciasNombreBanco,\n },\n },\n ...(hasCuentaMalDigitada\n ? [\n {\n properties: {\n sheetId:\n sheetIds.cuentaMalDigitada,\n title:\n sheetTitles.cuentaMalDigitada,\n },\n },\n ]\n : []),\n {\n properties: {\n sheetId:\n sheetIds.resumen,\n title:\n sheetTitles.resumen,\n },\n },\n ],\n },\n valueBatchBody: {\n valueInputOption:\n 'RAW',\n data: valueData,\n },\n formatBatchBody: {\n requests: formatRequests,\n },\n originalResponse: data,\n },\n },\n];" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 7616, - 6368 + 7264, + 7296 ], - "id": "9ab423de-de88-465c-90da-55f88097fb9d", + "id": "0269ecb0-63b1-43f6-9f34-d18d97a104b4", "name": "Preparar Google Sheet" }, { @@ -1492,10 +1492,10 @@ "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.4, "position": [ - 7872, - 6368 + 7520, + 7296 ], - "id": "981f50d3-0594-4dca-90f1-dfb6b5d38d60", + "id": "1ef3b281-554e-41dd-b95f-28fa3a1e70be", "name": "Crear Google Sheet", "credentials": { "httpBasicAuth": { @@ -1522,10 +1522,10 @@ "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.4, "position": [ - 8128, - 6368 + 7776, + 7296 ], - "id": "8230bd87-f59c-4df9-86e9-cc55f18111fc", + "id": "cf220e3d-0954-4b08-aa48-7d90afbb099d", "name": "Escribir Google Sheet", "credentials": { "googleOAuth2Api": { @@ -1548,10 +1548,10 @@ "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.4, "position": [ - 8384, - 6368 + 8032, + 7296 ], - "id": "007569d6-9d90-40e6-8fac-05dc52c2e7b6", + "id": "1e76a3ba-00f2-4455-ac64-e1f377e3d309", "name": "Formatear Google Sheet", "credentials": { "googleOAuth2Api": { @@ -1567,10 +1567,10 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 8656, - 6368 + 8304, + 7296 ], - "id": "e9a34bf2-dfd2-419d-adad-8d19e4249524", + "id": "bba45803-df53-41eb-9f04-988fc9456a3c", "name": "Preparar permisos Google Sheet" }, { @@ -1587,10 +1587,10 @@ "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.4, "position": [ - 8912, - 6368 + 8560, + 7296 ], - "id": "fe5677e2-b62e-483f-85ed-febbd122944a", + "id": "00d29e58-0e73-4b96-8848-f3012b30189b", "name": "Compartir Google Sheet", "credentials": { "googleOAuth2Api": { @@ -1606,10 +1606,10 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 10256, - 6368 + 9904, + 7296 ], - "id": "a238b381-4aeb-4be2-a791-d12e1de986f8", + "id": "09cfaf1e-f278-4f34-b3f2-09ccbc37c10d", "name": "Preparar histórico Supabase" }, { @@ -1645,10 +1645,10 @@ "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.4, "position": [ - 10512, - 6368 + 10160, + 7296 ], - "id": "67e475c5-d8b6-4a7f-a5f3-b195c53bd1ea", + "id": "a828f0bf-44ef-452c-8aae-f089b34aace5", "name": "Insertar histórico Supabase", "onError": "continueRegularOutput" }, @@ -1659,10 +1659,10 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 10784, - 6368 + 10432, + 7296 ], - "id": "b1fffa33-91ed-489d-8875-d4ed95a10288", + "id": "4321d98f-7d24-49d5-a03f-b19eb407f11e", "name": "Preparar respuesta final" }, { @@ -1684,10 +1684,10 @@ "type": "n8n-nodes-base.respondToWebhook", "typeVersion": 1.5, "position": [ - 11040, - 6368 + 10688, + 7296 ], - "id": "c6d47f4e-8e5e-4be9-bf7e-3f0b44065b69", + "id": "5f85312f-d57b-462b-943a-a7339f81e69d", "name": "Respond to Webhook" }, { @@ -1700,10 +1700,10 @@ "type": "n8n-nodes-base.stickyNote", "typeVersion": 1, "position": [ - 2928, - 5696 + 2576, + 6624 ], - "id": "6ae02113-7126-4271-be54-768996b76ca1", + "id": "d209ed03-9a6c-4377-aeb3-a25361644e5f", "name": "Sticky Note" }, { @@ -1716,10 +1716,10 @@ "type": "n8n-nodes-base.stickyNote", "typeVersion": 1, "position": [ - 4880, - 5632 + 4528, + 6560 ], - "id": "379d7a07-eb53-47c2-9223-33c2cf12cc00", + "id": "17197e7f-e073-464e-b8c8-ff0856c584d7", "name": "Sticky Note1" }, { @@ -1732,10 +1732,10 @@ "type": "n8n-nodes-base.stickyNote", "typeVersion": 1, "position": [ - 7104, - 6016 + 6752, + 6944 ], - "id": "c180d1f6-f9f9-41c9-b471-972062de1f50", + "id": "656ae2c7-4c04-4346-90de-1e4db21a637d", "name": "Sticky Note2" }, { @@ -1748,10 +1748,10 @@ "type": "n8n-nodes-base.stickyNote", "typeVersion": 1, "position": [ - 9296, - 6016 + 8944, + 6944 ], - "id": "20a6ed17-e087-4dca-b528-a9e076b9c9c7", + "id": "ac5189a6-802b-4f5e-a946-2aa574408974", "name": "Sticky Note3" } ], @@ -2139,33 +2139,17 @@ } }, "authors": "Isaac Aracena", - "name": "Version cd06be43", + "name": "Version 2be11ec0", "description": "", "autosaved": false, "workflowPublishHistory": [ { - "createdAt": "2026-07-22T15:11:26.969Z", - "id": 3380, + "createdAt": "2026-07-27T15:01:26.945Z", + "id": 3498, "workflowId": "5AujMxduslftVg9z", - "versionId": "cd06be43-5481-4a32-b10d-eb7434fab10d", + "versionId": "2be11ec0-4659-47ca-9a58-38fd0f9eb4d9", "event": "activated", "userId": "0a88c0b1-928e-4412-896e-c5d1c99b2029" - }, - { - "createdAt": "2026-07-25T17:28:34.734Z", - "id": 3460, - "workflowId": "5AujMxduslftVg9z", - "versionId": "cd06be43-5481-4a32-b10d-eb7434fab10d", - "event": "activated", - "userId": "0a88c0b1-928e-4412-896e-c5d1c99b2029" - }, - { - "createdAt": "2026-07-25T17:28:34.666Z", - "id": 3459, - "workflowId": "5AujMxduslftVg9z", - "versionId": "cd06be43-5481-4a32-b10d-eb7434fab10d", - "event": "deactivated", - "userId": "0a88c0b1-928e-4412-896e-c5d1c99b2029" } ] }