{ "updatedAt": "2026-06-20T14:26:50.284Z", "createdAt": "2026-05-27T15:07:19.943Z", "id": "MU5GdxLtFzcRcfyM", "name": "Tablero CDC - Sync Listas", "description": null, "active": true, "isArchived": false, "nodes": [ { "parameters": { "documentId": { "__rl": true, "value": "1MfoIs_y7-4ad-g5dxzOo8oAFIdraA_ZT4k-OCGMtT4Q", "mode": "list", "cachedResultName": "APROBACIONES PROYECTOS", "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1MfoIs_y7-4ad-g5dxzOo8oAFIdraA_ZT4k-OCGMtT4Q/edit?usp=drivesdk" }, "sheetName": { "__rl": true, "value": 1332738713, "mode": "list", "cachedResultName": "listas", "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1MfoIs_y7-4ad-g5dxzOo8oAFIdraA_ZT4k-OCGMtT4Q/edit#gid=1332738713" }, "options": {} }, "type": "n8n-nodes-base.googleSheets", "typeVersion": 4.7, "position": [ 208, 0 ], "id": "cd67f274-a366-4efb-841a-3a8bceda956f", "name": "Sheets - Leer listas", "credentials": { "googleSheetsOAuth2Api": { "id": "K0hDZh3a85MpOHCs", "name": "Google Sheets account 2" } } }, { "parameters": { "jsCode": "function normalizeText(value) {\n return String(value || '')\n .replace(/\\r\\n/g, '\\n')\n .replace(/\\s+/g, ' ')\n .trim();\n}\n\nfunction normalizeKey(value) {\n return normalizeText(value)\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .toLowerCase()\n .replace(/\\s+/g, ' ')\n .trim();\n}\n\nfunction pick(row, possibleHeaders) {\n const normalizedRow = {};\n\n for (const [key, value] of Object.entries(row || {})) {\n normalizedRow[normalizeKey(key)] = value;\n }\n\n for (const header of possibleHeaders) {\n const value = normalizedRow[normalizeKey(header)];\n\n if (value !== undefined && value !== null && String(value).trim() !== '') {\n return normalizeText(value);\n }\n }\n\n return '';\n}\n\nfunction splitValues(value) {\n const text = normalizeText(value);\n if (!text) return [];\n\n return text\n .split(/\\n|;/)\n .map(item => normalizeText(item))\n .filter(Boolean);\n}\n\nfunction isInvalidValue(value) {\n const invalidValues = new Set([\n 'no especificado',\n 'no especificada',\n 'n/a',\n 'na',\n '-',\n '--',\n '',\n ]);\n\n return invalidValues.has(normalizeKey(value));\n}\n\nfunction addRecord(records, category, value, label, sortOrder) {\n const cleanValue = normalizeText(value);\n const cleanLabel = normalizeText(label || value);\n\n if (!cleanValue || !cleanLabel) return;\n if (isInvalidValue(cleanValue)) return;\n\n records.push({\n category,\n value: cleanValue,\n label: cleanLabel,\n sort_order: sortOrder,\n is_active: true,\n });\n}\n\nconst rows = $input.all().map(item => item.json || {});\n\nconst records = [];\nlet sortOrder = 1;\n\nfor (const row of rows) {\n const client = pick(row, [\n 'Cliente',\n 'Clientes',\n 'CLIENTE',\n 'client',\n ]);\n\n const brand = pick(row, [\n 'Marca',\n 'Marcas',\n 'MARCA',\n 'brand',\n ]);\n\n const country = pick(row, [\n 'Pais / BU',\n 'País / BU',\n 'Pais',\n 'País',\n 'Business Unit',\n 'BU',\n 'Country',\n ]);\n\n const countryManager = pick(row, [\n 'CM',\n 'Country Manager',\n 'County Manager',\n 'Manager',\n ]);\n\n const workedBy = pick(row, [\n 'Trabajado',\n 'Trabajado por',\n 'Responsable',\n 'Diseñador',\n 'Creativo',\n ]);\n\n const workType = pick(row, [\n 'Tipo de trabajo',\n 'Tipo Trabajo',\n 'Trabajo',\n 'Tipo',\n ]);\n\n const executedWon = pick(row, [\n 'Ejecutado / Ganado',\n 'Ejecutado/Ganado',\n 'Ejecutado',\n 'Ganado',\n 'Resultado',\n ]);\n\n const timeStatus = pick(row, [\n 'Time',\n 'Tiempo',\n 'Estado tiempo',\n 'Estado de tiempo',\n ]);\n\n const status = pick(row, [\n 'estatus',\n 'Estatus',\n 'Status',\n 'Estado',\n 'ESTATUS',\n ]);\n\n const month = pick(row, [\n 'mes',\n 'Mes',\n 'MES',\n 'month',\n ]);\n\n for (const value of splitValues(client)) {\n addRecord(records, 'client', value, value, sortOrder++);\n }\n\n for (const value of splitValues(brand)) {\n addRecord(records, 'brand', value, value, sortOrder++);\n }\n\n for (const value of splitValues(country)) {\n addRecord(records, 'country', value, value, sortOrder++);\n }\n\n if (country && countryManager) {\n addRecord(records, 'country_manager', country, countryManager, sortOrder++);\n }\n\n for (const value of splitValues(workedBy)) {\n addRecord(records, 'worked_by', value, value, sortOrder++);\n }\n\n for (const value of splitValues(workType)) {\n addRecord(records, 'work_type', value, value, sortOrder++);\n }\n\n for (const value of splitValues(executedWon)) {\n addRecord(records, 'execution_result', value, value, sortOrder++);\n }\n\n for (const value of splitValues(timeStatus)) {\n addRecord(records, 'time_status', value, value, sortOrder++);\n }\n\n for (const value of splitValues(status)) {\n addRecord(records, 'status', value, value, sortOrder++);\n }\n\n for (const value of splitValues(month)) {\n addRecord(records, 'month', value, value, sortOrder++);\n }\n}\n\n// Deduplicar por category + value\nconst uniqueMap = new Map();\n\nfor (const record of records) {\n const key = `${normalizeKey(record.category)}::${normalizeKey(record.value)}`;\n\n if (!uniqueMap.has(key)) {\n uniqueMap.set(key, record);\n }\n}\n\nconst uniqueRecords = Array.from(uniqueMap.values())\n .map((record, index) => ({\n ...record,\n sort_order: index + 1,\n }));\n\nreturn [\n {\n json: {\n total_rows_read: rows.length,\n total_records_to_sync: uniqueRecords.length,\n categories_found: [...new Set(uniqueRecords.map(item => item.category))],\n records: uniqueRecords,\n preview: uniqueRecords.slice(0, 50),\n },\n },\n];" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 416, 0 ], "id": "6c6131fe-f6df-434c-9596-c9d29a90ed0a", "name": "Code - Normalizar listas para Supabase" }, { "parameters": { "method": "POST", "url": "https://dbit.digitalcompass.agency/rest/v1/tablero_cdc_app_lists?on_conflict=category,value", "sendHeaders": true, "headerParameters": { "parameters": [ { "name": "apikey", "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q" }, { "name": "Authorization", "value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q" }, { "name": "Content-Type", "value": "application/json" }, { "name": "Prefer", "value": "resolution=merge-duplicates,return=representation" } ] }, "sendBody": true, "specifyBody": "json", "jsonBody": "={{ JSON.stringify($json.records) }}", "options": {} }, "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.4, "position": [ 624, 0 ], "id": "df494429-1c90-4e18-9bb1-d9b0d9b46cad", "name": "Supabase - Upsert app_lists" }, { "parameters": { "content": "## Flujo que se ejecuta cada vez que se agrega una fila nueva, lee las listas del Sheet luego normaliza las listas y por último hace el Upsert en Supabase", "height": 352, "width": 912 }, "type": "n8n-nodes-base.stickyNote", "typeVersion": 1, "position": [ -64, -144 ], "id": "2ec4d347-68e4-4358-9a82-3748c01af9a6", "name": "Sticky Note" }, { "parameters": { "rule": { "interval": [ { "field": "hours", "hoursInterval": 8 } ] } }, "type": "n8n-nodes-base.scheduleTrigger", "typeVersion": 1.3, "position": [ 16, 0 ], "id": "4639502f-e8b1-4373-8f7f-d122598d2051", "name": "Schedule Trigger" } ], "connections": { "Sheets - Leer listas": { "main": [ [ { "node": "Code - Normalizar listas para Supabase", "type": "main", "index": 0 } ] ] }, "Code - Normalizar listas para Supabase": { "main": [ [ { "node": "Supabase - Upsert app_lists", "type": "main", "index": 0 } ] ] }, "Schedule Trigger": { "main": [ [ { "node": "Sheets - Leer listas", "type": "main", "index": 0 } ] ] } }, "settings": { "executionOrder": "v1", "binaryMode": "separate", "timeSavedMode": "fixed", "errorWorkflow": "puF4LUczoSz3hcek", "callerPolicy": "workflowsFromSameOwner", "availableInMCP": false }, "staticData": { "node:Schedule Trigger": { "recurrenceRules": [ 16 ] }, "node:Google Sheets Trigger": { "documentId": "1MfoIs_y7-4ad-g5dxzOo8oAFIdraA_ZT4k-OCGMtT4Q", "sheetId": 1332738713, "lastIndexChecked": 91, "lastRevision": 9616, "lastRevisionLink": "https://docs.google.com/spreadsheets/export?id=1MfoIs_y7-4ad-g5dxzOo8oAFIdraA_ZT4k-OCGMtT4Q&revision=9616&exportFormat=xlsx" } }, "meta": { "templateCredsSetupCompleted": true }, "versionId": "a92cce4a-174a-4e09-92bd-2062fa41bc8d", "activeVersionId": "a92cce4a-174a-4e09-92bd-2062fa41bc8d", "versionCounter": 145, "triggerCount": 1, "shared": [ { "updatedAt": "2026-05-27T15:07:19.948Z", "createdAt": "2026-05-27T15:07:19.948Z", "role": "workflow:owner", "workflowId": "MU5GdxLtFzcRcfyM", "projectId": "PJpTANzTXIFibWsW", "project": { "updatedAt": "2026-04-22T14:25:09.686Z", "createdAt": "2026-04-22T14:22:54.790Z", "id": "PJpTANzTXIFibWsW", "name": "Isaac Aracena ", "type": "personal", "icon": null, "description": null, "creatorId": "0a88c0b1-928e-4412-896e-c5d1c99b2029" } } ], "tags": [], "activeVersion": { "updatedAt": "2026-06-20T14:26:53.000Z", "createdAt": "2026-06-20T14:26:50.285Z", "versionId": "a92cce4a-174a-4e09-92bd-2062fa41bc8d", "workflowId": "MU5GdxLtFzcRcfyM", "nodes": [ { "parameters": { "documentId": { "__rl": true, "value": "1MfoIs_y7-4ad-g5dxzOo8oAFIdraA_ZT4k-OCGMtT4Q", "mode": "list", "cachedResultName": "APROBACIONES PROYECTOS", "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1MfoIs_y7-4ad-g5dxzOo8oAFIdraA_ZT4k-OCGMtT4Q/edit?usp=drivesdk" }, "sheetName": { "__rl": true, "value": 1332738713, "mode": "list", "cachedResultName": "listas", "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1MfoIs_y7-4ad-g5dxzOo8oAFIdraA_ZT4k-OCGMtT4Q/edit#gid=1332738713" }, "options": {} }, "type": "n8n-nodes-base.googleSheets", "typeVersion": 4.7, "position": [ 208, 0 ], "id": "cd67f274-a366-4efb-841a-3a8bceda956f", "name": "Sheets - Leer listas", "credentials": { "googleSheetsOAuth2Api": { "id": "K0hDZh3a85MpOHCs", "name": "Google Sheets account 2" } } }, { "parameters": { "jsCode": "function normalizeText(value) {\n return String(value || '')\n .replace(/\\r\\n/g, '\\n')\n .replace(/\\s+/g, ' ')\n .trim();\n}\n\nfunction normalizeKey(value) {\n return normalizeText(value)\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .toLowerCase()\n .replace(/\\s+/g, ' ')\n .trim();\n}\n\nfunction pick(row, possibleHeaders) {\n const normalizedRow = {};\n\n for (const [key, value] of Object.entries(row || {})) {\n normalizedRow[normalizeKey(key)] = value;\n }\n\n for (const header of possibleHeaders) {\n const value = normalizedRow[normalizeKey(header)];\n\n if (value !== undefined && value !== null && String(value).trim() !== '') {\n return normalizeText(value);\n }\n }\n\n return '';\n}\n\nfunction splitValues(value) {\n const text = normalizeText(value);\n if (!text) return [];\n\n return text\n .split(/\\n|;/)\n .map(item => normalizeText(item))\n .filter(Boolean);\n}\n\nfunction isInvalidValue(value) {\n const invalidValues = new Set([\n 'no especificado',\n 'no especificada',\n 'n/a',\n 'na',\n '-',\n '--',\n '',\n ]);\n\n return invalidValues.has(normalizeKey(value));\n}\n\nfunction addRecord(records, category, value, label, sortOrder) {\n const cleanValue = normalizeText(value);\n const cleanLabel = normalizeText(label || value);\n\n if (!cleanValue || !cleanLabel) return;\n if (isInvalidValue(cleanValue)) return;\n\n records.push({\n category,\n value: cleanValue,\n label: cleanLabel,\n sort_order: sortOrder,\n is_active: true,\n });\n}\n\nconst rows = $input.all().map(item => item.json || {});\n\nconst records = [];\nlet sortOrder = 1;\n\nfor (const row of rows) {\n const client = pick(row, [\n 'Cliente',\n 'Clientes',\n 'CLIENTE',\n 'client',\n ]);\n\n const brand = pick(row, [\n 'Marca',\n 'Marcas',\n 'MARCA',\n 'brand',\n ]);\n\n const country = pick(row, [\n 'Pais / BU',\n 'País / BU',\n 'Pais',\n 'País',\n 'Business Unit',\n 'BU',\n 'Country',\n ]);\n\n const countryManager = pick(row, [\n 'CM',\n 'Country Manager',\n 'County Manager',\n 'Manager',\n ]);\n\n const workedBy = pick(row, [\n 'Trabajado',\n 'Trabajado por',\n 'Responsable',\n 'Diseñador',\n 'Creativo',\n ]);\n\n const workType = pick(row, [\n 'Tipo de trabajo',\n 'Tipo Trabajo',\n 'Trabajo',\n 'Tipo',\n ]);\n\n const executedWon = pick(row, [\n 'Ejecutado / Ganado',\n 'Ejecutado/Ganado',\n 'Ejecutado',\n 'Ganado',\n 'Resultado',\n ]);\n\n const timeStatus = pick(row, [\n 'Time',\n 'Tiempo',\n 'Estado tiempo',\n 'Estado de tiempo',\n ]);\n\n const status = pick(row, [\n 'estatus',\n 'Estatus',\n 'Status',\n 'Estado',\n 'ESTATUS',\n ]);\n\n const month = pick(row, [\n 'mes',\n 'Mes',\n 'MES',\n 'month',\n ]);\n\n for (const value of splitValues(client)) {\n addRecord(records, 'client', value, value, sortOrder++);\n }\n\n for (const value of splitValues(brand)) {\n addRecord(records, 'brand', value, value, sortOrder++);\n }\n\n for (const value of splitValues(country)) {\n addRecord(records, 'country', value, value, sortOrder++);\n }\n\n if (country && countryManager) {\n addRecord(records, 'country_manager', country, countryManager, sortOrder++);\n }\n\n for (const value of splitValues(workedBy)) {\n addRecord(records, 'worked_by', value, value, sortOrder++);\n }\n\n for (const value of splitValues(workType)) {\n addRecord(records, 'work_type', value, value, sortOrder++);\n }\n\n for (const value of splitValues(executedWon)) {\n addRecord(records, 'execution_result', value, value, sortOrder++);\n }\n\n for (const value of splitValues(timeStatus)) {\n addRecord(records, 'time_status', value, value, sortOrder++);\n }\n\n for (const value of splitValues(status)) {\n addRecord(records, 'status', value, value, sortOrder++);\n }\n\n for (const value of splitValues(month)) {\n addRecord(records, 'month', value, value, sortOrder++);\n }\n}\n\n// Deduplicar por category + value\nconst uniqueMap = new Map();\n\nfor (const record of records) {\n const key = `${normalizeKey(record.category)}::${normalizeKey(record.value)}`;\n\n if (!uniqueMap.has(key)) {\n uniqueMap.set(key, record);\n }\n}\n\nconst uniqueRecords = Array.from(uniqueMap.values())\n .map((record, index) => ({\n ...record,\n sort_order: index + 1,\n }));\n\nreturn [\n {\n json: {\n total_rows_read: rows.length,\n total_records_to_sync: uniqueRecords.length,\n categories_found: [...new Set(uniqueRecords.map(item => item.category))],\n records: uniqueRecords,\n preview: uniqueRecords.slice(0, 50),\n },\n },\n];" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 416, 0 ], "id": "6c6131fe-f6df-434c-9596-c9d29a90ed0a", "name": "Code - Normalizar listas para Supabase" }, { "parameters": { "method": "POST", "url": "https://dbit.digitalcompass.agency/rest/v1/tablero_cdc_app_lists?on_conflict=category,value", "sendHeaders": true, "headerParameters": { "parameters": [ { "name": "apikey", "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q" }, { "name": "Authorization", "value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q" }, { "name": "Content-Type", "value": "application/json" }, { "name": "Prefer", "value": "resolution=merge-duplicates,return=representation" } ] }, "sendBody": true, "specifyBody": "json", "jsonBody": "={{ JSON.stringify($json.records) }}", "options": {} }, "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.4, "position": [ 624, 0 ], "id": "df494429-1c90-4e18-9bb1-d9b0d9b46cad", "name": "Supabase - Upsert app_lists" }, { "parameters": { "content": "## Flujo que se ejecuta cada vez que se agrega una fila nueva, lee las listas del Sheet luego normaliza las listas y por último hace el Upsert en Supabase", "height": 352, "width": 912 }, "type": "n8n-nodes-base.stickyNote", "typeVersion": 1, "position": [ -64, -144 ], "id": "2ec4d347-68e4-4358-9a82-3748c01af9a6", "name": "Sticky Note" }, { "parameters": { "rule": { "interval": [ { "field": "hours", "hoursInterval": 8 } ] } }, "type": "n8n-nodes-base.scheduleTrigger", "typeVersion": 1.3, "position": [ 16, 0 ], "id": "4639502f-e8b1-4373-8f7f-d122598d2051", "name": "Schedule Trigger" } ], "connections": { "Sheets - Leer listas": { "main": [ [ { "node": "Code - Normalizar listas para Supabase", "type": "main", "index": 0 } ] ] }, "Code - Normalizar listas para Supabase": { "main": [ [ { "node": "Supabase - Upsert app_lists", "type": "main", "index": 0 } ] ] }, "Schedule Trigger": { "main": [ [ { "node": "Sheets - Leer listas", "type": "main", "index": 0 } ] ] } }, "authors": "Isaac Aracena", "name": "Version a92cce4a", "description": "", "autosaved": true, "workflowPublishHistory": [ { "createdAt": "2026-06-20T14:26:53.417Z", "id": 1672, "workflowId": "MU5GdxLtFzcRcfyM", "versionId": "a92cce4a-174a-4e09-92bd-2062fa41bc8d", "event": "activated", "userId": "0a88c0b1-928e-4412-896e-c5d1c99b2029" } ] } }