From 3d6908ce2c992782fd4d9dc70329192a46b91615 Mon Sep 17 00:00:00 2001 From: Isaac_Aracena Date: Fri, 5 Jun 2026 14:43:49 +0000 Subject: [PATCH] Create backup: Tablero CDC - Sync Listas --- tablero-cdc-sync-listas.json | 397 +++++++++++++++++++++++++++++++++++ 1 file changed, 397 insertions(+) create mode 100644 tablero-cdc-sync-listas.json diff --git a/tablero-cdc-sync-listas.json b/tablero-cdc-sync-listas.json new file mode 100644 index 0000000..c085c8c --- /dev/null +++ b/tablero-cdc-sync-listas.json @@ -0,0 +1,397 @@ +{ + "updatedAt": "2026-06-04T00:42:19.112Z", + "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": { + "rule": { + "interval": [ + { + "field": "minutes" + } + ] + } + }, + "type": "n8n-nodes-base.scheduleTrigger", + "typeVersion": 1.3, + "position": [ + 0, + 0 + ], + "id": "05c35d3e-0dc2-4113-a573-2a14b59d46b3", + "name": "Schedule Trigger" + }, + { + "parameters": { + "content": "Flujo que se ejecuta cada 5 minutos, 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" + } + ], + "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": [] + } + }, + "meta": { + "templateCredsSetupCompleted": true + }, + "versionId": "afed7da6-4c0c-47ef-b38e-e12fbd1e79e1", + "activeVersionId": "afed7da6-4c0c-47ef-b38e-e12fbd1e79e1", + "versionCounter": 79, + "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-03T21:05:24.000Z", + "createdAt": "2026-06-03T21:05:14.537Z", + "versionId": "afed7da6-4c0c-47ef-b38e-e12fbd1e79e1", + "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": { + "rule": { + "interval": [ + { + "field": "minutes" + } + ] + } + }, + "type": "n8n-nodes-base.scheduleTrigger", + "typeVersion": 1.3, + "position": [ + 0, + 0 + ], + "id": "05c35d3e-0dc2-4113-a573-2a14b59d46b3", + "name": "Schedule Trigger" + }, + { + "parameters": { + "content": "Flujo que se ejecuta cada 5 minutos, 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" + } + ], + "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": "Tablero CDC - Sync Listas", + "description": "", + "autosaved": false, + "workflowPublishHistory": [ + { + "createdAt": "2026-06-03T21:05:24.337Z", + "id": 1068, + "workflowId": "MU5GdxLtFzcRcfyM", + "versionId": "afed7da6-4c0c-47ef-b38e-e12fbd1e79e1", + "event": "activated", + "userId": "0a88c0b1-928e-4412-896e-c5d1c99b2029" + }, + { + "createdAt": "2026-06-04T00:42:19.189Z", + "id": 1074, + "workflowId": "MU5GdxLtFzcRcfyM", + "versionId": "afed7da6-4c0c-47ef-b38e-e12fbd1e79e1", + "event": "activated", + "userId": "0a88c0b1-928e-4412-896e-c5d1c99b2029" + }, + { + "createdAt": "2026-06-04T00:42:19.157Z", + "id": 1073, + "workflowId": "MU5GdxLtFzcRcfyM", + "versionId": "afed7da6-4c0c-47ef-b38e-e12fbd1e79e1", + "event": "deactivated", + "userId": "0a88c0b1-928e-4412-896e-c5d1c99b2029" + } + ] + } +} \ No newline at end of file