Files
tablero-cdc-sync-listas/tablero-cdc-sync-listas.json
T

392 lines
21 KiB
JSON

{
"updatedAt": "2026-06-23T20:18:23.557Z",
"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": 3
}
]
}
},
"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": [
0
]
},
"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": "0a67de07-00b8-42d8-8b97-6ae8f6b675a5",
"activeVersionId": "0a67de07-00b8-42d8-8b97-6ae8f6b675a5",
"versionCounter": 193,
"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 <iaracena@gomezleemarketing.com>",
"type": "personal",
"icon": null,
"description": null,
"creatorId": "0a88c0b1-928e-4412-896e-c5d1c99b2029"
}
}
],
"tags": [],
"activeVersion": {
"updatedAt": "2026-06-23T20:18:27.000Z",
"createdAt": "2026-06-23T20:18:23.559Z",
"versionId": "0a67de07-00b8-42d8-8b97-6ae8f6b675a5",
"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": 3
}
]
}
},
"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 0a67de07",
"description": "",
"autosaved": true,
"workflowPublishHistory": [
{
"createdAt": "2026-06-23T20:18:27.921Z",
"id": 1782,
"workflowId": "MU5GdxLtFzcRcfyM",
"versionId": "0a67de07-00b8-42d8-8b97-6ae8f6b675a5",
"event": "activated",
"userId": "0a88c0b1-928e-4412-896e-c5d1c99b2029"
}
]
}
}