diff --git a/Tablero CDC - Migrar Legacy Sheet a Supabase.json b/Tablero CDC - Migrar Legacy Sheet a Supabase.json new file mode 100644 index 0000000..aa7bb95 --- /dev/null +++ b/Tablero CDC - Migrar Legacy Sheet a Supabase.json @@ -0,0 +1,525 @@ +{ + "name": "Tablero CDC - Migrar Legacy Sheet a Supabase", + "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": 1563472127, + "mode": "list", + "cachedResultName": "PROYECTOS 2026", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1MfoIs_y7-4ad-g5dxzOo8oAFIdraA_ZT4k-OCGMtT4Q/edit#gid=1563472127" + }, + "options": { + "dataLocationOnSheet": { + "values": { + "rangeDefinition": "specifyRangeA1", + "range": "A3:N183" + } + } + } + }, + "type": "n8n-nodes-base.googleSheets", + "typeVersion": 4.7, + "position": [ + 160, + -16 + ], + "id": "c2e04d79-3014-49ec-b213-931cf57fa5f1", + "name": "Sheets - Leer legacy rows", + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + } + }, + { + "parameters": { + "jsCode": "const rows = $input.all();\n\nconst FIRST_DATA_ROW = 4;\n\n// PRUEBA SEGURA:\n// Solo migramos la fila 4 por ahora.\n// Cuando validemos, cambiamos esto a 183.\nconst LAST_LEGACY_ROW = 183;\n\nfunction clean(value) {\n return String(value ?? '').trim();\n}\n\nfunction createUuid() {\n return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (char) {\n const random = Math.random() * 16 | 0;\n const value = char === 'x' ? random : (random & 0x3 | 0x8);\n return value.toString(16);\n });\n}\n\nfunction parseAmount(value) {\n const text = clean(value);\n if (!text) return null;\n\n const normalized = text\n .replace(/RD\\$/gi, '')\n .replace(/DOP/gi, '')\n .replace(/,/g, '')\n .trim();\n\n const number = Number(normalized);\n return Number.isFinite(number) ? number : null;\n}\n\nfunction extractRequestedBy(comments) {\n const text = clean(comments);\n const match = text.match(/^solicitado\\s+por\\s+(.+)$/i);\n return match ? match[1].trim() : '';\n}\n\nconst output = [];\n\nrows.forEach((item, index) => {\n const row = item.json;\n\n const sheetRowNumber = FIRST_DATA_ROW + index;\n\n if (sheetRowNumber < FIRST_DATA_ROW || sheetRowNumber > LAST_LEGACY_ROW) {\n return;\n }\n\n const existingProjectId = clean(row['Project ID']);\n if (existingProjectId) {\n return;\n }\n\n const title = clean(row['Tipo o nombre de proyecto']);\n const client = clean(row['Cliente']);\n const country = clean(row['BU Solicita']);\n const brand = clean(row['Marca']);\n const countryManager = clean(row['CM']);\n const status = clean(row['Status']) || 'Activo';\n const comments = clean(row['Comentarios']);\n const proposalLink = clean(row['Link de propuesta']);\n const internalAmount = parseAmount(row['$ Interno Cargado']);\n const briefLink = clean(row['Link del Brief']);\n const finalArtLink = clean(row['Link de artes finales']);\n const year = clean(row['Año']);\n const month = clean(row['Mes']);\n\n if (!title || !client || !country || !brand) {\n throw new Error(\n `Fila ${sheetRowNumber}: faltan campos mínimos. title=\"${title}\", client=\"${client}\", country=\"${country}\", brand=\"${brand}\"`\n );\n }\n\n const projectId = createUuid();\n\n const projectPayload = {\n id: projectId,\n title,\n client,\n brand,\n country,\n requested_by: extractRequestedBy(comments) || null,\n country_manager: countryManager || null,\n description: comments || null,\n status,\n internal_amount: internalAmount,\n currency: 'DOP',\n brief_link: briefLink || null,\n extra_data: {\n legacy_migration: true,\n legacy_source: 'PROYECTOS 2026',\n legacy_sheet_row_number: sheetRowNumber,\n legacy_year: year,\n legacy_month: month,\n migrated_at: new Date().toISOString(),\n },\n };\n\n const links = [];\n\n if (proposalLink) {\n links.push({\n project_id: projectId,\n link_type: 'proposal',\n url: proposalLink,\n label: 'Propuesta legacy',\n extra_data: {\n legacy_migration: true,\n legacy_sheet_row_number: sheetRowNumber,\n },\n });\n }\n\n if (finalArtLink) {\n links.push({\n project_id: projectId,\n link_type: 'final_art',\n url: finalArtLink,\n label: 'Artes finales legacy',\n extra_data: {\n legacy_migration: true,\n legacy_sheet_row_number: sheetRowNumber,\n },\n });\n }\n\n output.push({\n json: {\n sheet_row_number: sheetRowNumber,\n project_id: projectId,\n project_id_cell: `N${sheetRowNumber}`,\n title,\n project_payload: projectPayload,\n links,\n },\n });\n});\n\nreturn output;" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 416, + -16 + ], + "id": "0303074a-5370-4bab-9fb8-e06e265d17aa", + "name": "Code - Preparar migracion legacy" + }, + { + "parameters": { + "method": "POST", + "url": "https://dbit.digitalcompass.agency/rest/v1/tablero_cdc_projects", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "apikey", + "value": "TU_SERVICE_ROLE_KEY_REAL" + }, + { + "name": "Authorization", + "value": "Bearer TU_SERVICE_ROLE_KEY_REAL" + }, + { + "name": "Content-Type", + "value": "application/json" + }, + { + "name": "Prefer", + "value": "return=minimal" + } + ] + }, + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={{ $json.project_payload }}", + "options": {} + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.4, + "position": [ + 624, + -16 + ], + "id": "6be4d8e1-13d5-4273-b5c5-d83b9ae28188", + "name": "Supabase - Insertar proyecto legacy" + }, + { + "parameters": { + "jsCode": "const preparedItems = $('Code - Preparar migracion legacy').all();\n\nconst output = [];\n\nfor (const item of preparedItems) {\n const links = item.json.links || [];\n\n for (const link of links) {\n output.push({\n json: link,\n });\n }\n}\n\nreturn output;" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 832, + -16 + ], + "id": "5c149992-85e2-4ade-a32f-e280131b0d5a", + "name": "Code - Preparar links legacy" + }, + { + "parameters": { + "method": "POST", + "url": "https://dbit.digitalcompass.agency/rest/v1/tablero_cdc_project_links", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "apikey", + "value": "TU_SERVICE_ROLE_KEY_REAL" + }, + { + "name": "Authorization", + "value": "Bearer TU_SERVICE_ROLE_KEY_REAL" + }, + { + "name": "Content-Type", + "value": "application/json" + }, + { + "name": "Prefer", + "value": "return=minimal" + } + ] + }, + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={{ $json }}", + "options": {} + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.4, + "position": [ + 1040, + -16 + ], + "id": "5968b76f-2846-4c2f-a643-a52116e544a3", + "name": "Supabase - Insertar links legacy" + }, + { + "parameters": { + "jsCode": "const preparedItems = $('Code - Preparar migracion legacy').all();\n\nreturn preparedItems.map(item => {\n return {\n json: {\n row_number: item.json.sheet_row_number,\n project_id: item.json.project_id,\n title: item.json.title,\n project_id_cell: item.json.project_id_cell,\n },\n };\n});" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1248, + -16 + ], + "id": "056908f7-3f2c-48d6-8613-90cd8eb3a3d3", + "name": "Code - Preparar Project IDs Sheet" + }, + { + "parameters": { + "operation": "update", + "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": 1563472127, + "mode": "list", + "cachedResultName": "PROYECTOS 2026", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1MfoIs_y7-4ad-g5dxzOo8oAFIdraA_ZT4k-OCGMtT4Q/edit#gid=1563472127" + }, + "columns": { + "mappingMode": "defineBelow", + "value": { + "row_number": "={{ $json.row_number }}", + "Project ID": "={{ $json.project_id }}" + }, + "matchingColumns": [ + "row_number" + ], + "schema": [ + { + "id": "Año", + "displayName": "Año", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "Mes", + "displayName": "Mes", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "Tipo o nombre de proyecto", + "displayName": "Tipo o nombre de proyecto", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "Cliente", + "displayName": "Cliente", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "BU Solicita", + "displayName": "BU Solicita", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "Marca", + "displayName": "Marca", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "CM", + "displayName": "CM", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "Status", + "displayName": "Status", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "Comentarios", + "displayName": "Comentarios", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "Link de propuesta", + "displayName": "Link de propuesta", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "$ Interno Cargado", + "displayName": "$ Interno Cargado", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "Link del Brief", + "displayName": "Link del Brief", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "Link de artes finales", + "displayName": "Link de artes finales", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "Project ID", + "displayName": "Project ID", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "row_number", + "displayName": "row_number", + "required": false, + "defaultMatch": false, + "display": true, + "type": "number", + "canBeUsedToMatch": true, + "readOnly": true, + "removed": false + } + ], + "attemptToConvertTypes": false, + "convertFieldsToString": false + }, + "options": { + "locationDefine": { + "values": { + "headerRow": 3, + "firstDataRow": 4 + } + } + } + }, + "type": "n8n-nodes-base.googleSheets", + "typeVersion": 4.7, + "position": [ + 1456, + -16 + ], + "id": "666a950b-cd80-4f0a-885b-80745e74bb0b", + "name": "Sheets - Escribir Project ID legacy", + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + } + }, + { + "parameters": { + "path": "tablero-cdc-migracion-legacy-archivada", + "responseMode": "responseNode", + "options": {} + }, + "type": "n8n-nodes-base.webhook", + "typeVersion": 2.1, + "position": [ + -304, + 16 + ], + "id": "9ba81599-f14c-45c7-b273-2ee1f6d2e8da", + "name": "Webhook", + "webhookId": "ba3e5457-3806-4e55-ac0b-9e4d90d3735c" + }, + { + "parameters": { + "respondWith": "json", + "responseBody": "{\n \"ok\": true,\n \"status\": \"archived\",\n \"workflow\": \"Tablero CDC - Migrar Legacy Sheet a Supabase\",\n \"message\": \"Workflow archivado solo para respaldo en Gitea. No ejecuta migración.\"\n}", + "options": { + "responseCode": 200 + } + }, + "type": "n8n-nodes-base.respondToWebhook", + "typeVersion": 1.5, + "position": [ + -48, + 16 + ], + "id": "aef53048-0059-46ce-9eb6-36be924d678b", + "name": "Respond - Workflow archivado" + }, + { + "parameters": { + "content": "## Migrar los proyectos históricos del Sheet PROYECTOS 2026 a Supabase.\n\nEl Webhook publicado es solo para que el flujo de respaldo a Gitea pueda guardar este workflow.\n\n", + "height": 336, + "width": 2048, + "color": "#33801E" + }, + "type": "n8n-nodes-base.stickyNote", + "typeVersion": 1, + "position": [ + -352, + -128 + ], + "id": "f45a8a78-a258-4934-b755-9f82792a6d69", + "name": "Sticky Note" + } + ], + "pinData": {}, + "connections": { + "Sheets - Leer legacy rows": { + "main": [ + [ + { + "node": "Code - Preparar migracion legacy", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar migracion legacy": { + "main": [ + [ + { + "node": "Supabase - Insertar proyecto legacy", + "type": "main", + "index": 0 + } + ] + ] + }, + "Supabase - Insertar proyecto legacy": { + "main": [ + [ + { + "node": "Code - Preparar links legacy", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar links legacy": { + "main": [ + [ + { + "node": "Supabase - Insertar links legacy", + "type": "main", + "index": 0 + } + ] + ] + }, + "Supabase - Insertar links legacy": { + "main": [ + [ + { + "node": "Code - Preparar Project IDs Sheet", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar Project IDs Sheet": { + "main": [ + [ + { + "node": "Sheets - Escribir Project ID legacy", + "type": "main", + "index": 0 + } + ] + ] + }, + "Webhook": { + "main": [ + [ + { + "node": "Respond - Workflow archivado", + "type": "main", + "index": 0 + } + ] + ] + }, + "Respond - Workflow archivado": { + "main": [ + [ + { + "node": "Sheets - Leer legacy rows", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": true, + "settings": { + "executionOrder": "v1", + "binaryMode": "separate", + "availableInMCP": true, + "timeSavedMode": "fixed", + "errorWorkflow": "puF4LUczoSz3hcek", + "timezone": "America/Santo_Domingo", + "callerPolicy": "workflowsFromSameOwner" + }, + "versionId": "e3cc75d8-5d2d-421f-9714-45efb1d62aa1", + "meta": { + "templateCredsSetupCompleted": true, + "instanceId": "b4b77b17af092830e794eef639ce2f6d7daccf7eddc075060b03b3b6545aac70" + }, + "id": "IZLgEpGSfWhjj6Qy", + "tags": [] +} \ No newline at end of file diff --git a/Tablero CDC - Sync Proyecto a Banco Fulgencio IA.json b/Tablero CDC - Sync Proyecto a Banco Fulgencio IA.json new file mode 100644 index 0000000..90e01b2 --- /dev/null +++ b/Tablero CDC - Sync Proyecto a Banco Fulgencio IA.json @@ -0,0 +1,775 @@ +{ + "name": "Tablero CDC - Sync Proyecto a Banco Fulgencio IA", + "nodes": [ + { + "parameters": { + "httpMethod": "POST", + "path": "tablero-cdc-sync-fulgencio-ia", + "responseMode": "responseNode", + "options": {} + }, + "type": "n8n-nodes-base.webhook", + "typeVersion": 2.1, + "position": [ + 0, + 0 + ], + "id": "b57d9cd2-3f7f-4e24-98e3-ef5ec131afc4", + "name": "Webhook", + "webhookId": "3b5cf1f9-a3e8-4511-82ce-188afabd5fe1" + }, + { + "parameters": { + "jsCode": "const input = $json || {};\nconst body = input.body || input;\n\nconst projectId =\n body.project_id ||\n body.projectId ||\n body.id ||\n '';\n\nconst action =\n String(body.action || 'sync').trim().toLowerCase();\n\nif (!projectId) {\n throw new Error('Falta project_id en el payload del webhook');\n}\n\nreturn [\n {\n json: {\n action,\n project_id: projectId,\n received_at: new Date().toISOString(),\n },\n },\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 208, + 0 + ], + "id": "2764c02c-8d8e-4edc-9dc3-48c0cd880b30", + "name": "Code - Preparar project_id" + }, + { + "parameters": { + "url": "=https://dbit.digitalcompass.agency/rest/v1/tablero_cdc_projects?id=eq.{{ $('Code - Preparar project_id').first().json.project_id }}&select=*", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "apikey", + "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q" + }, + { + "name": "Authorization", + "value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q" + }, + { + "name": "Accept", + "value": " application/json" + }, + { + "name": "Content-Type", + "value": "application/json" + } + ] + }, + "options": {} + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.4, + "position": [ + 416, + 0 + ], + "id": "8db634e3-f411-4701-9845-673b5db1121f", + "name": "Supabase - Leer proyecto" + }, + { + "parameters": { + "url": "=https://dbit.digitalcompass.agency/rest/v1/tablero_cdc_project_links?project_id=eq.{{ $('Code - Preparar project_id').first().json.project_id }}&select=*&order=created_at.asc", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "apikey", + "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q" + }, + { + "name": "Authorization", + "value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q" + }, + { + "name": "Accept", + "value": " application/json" + }, + { + "name": "Content-Type", + "value": "application/json" + } + ] + }, + "options": {} + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.4, + "position": [ + 592, + 0 + ], + "id": "6d151961-e0ae-4049-b3a2-88bae2494f8c", + "name": "Supabase - Leer links del proyecto", + "alwaysOutputData": true + }, + { + "parameters": { + "jsCode": "function getNodeJson(nodeName) {\n try {\n return $(nodeName).first().json;\n } catch (error) {\n return {};\n }\n}\n\nfunction getNodeItems(nodeName) {\n try {\n return $(nodeName).all();\n } catch (error) {\n return [];\n }\n}\n\nfunction normalizeProjectResponse(value) {\n if (Array.isArray(value)) return value[0] || null;\n return value || null;\n}\n\nfunction normalizeLinksResponse(items) {\n const links = [];\n\n for (const item of items) {\n const json = item.json;\n\n if (Array.isArray(json)) {\n links.push(...json);\n } else if (json && typeof json === 'object') {\n links.push(json);\n }\n }\n\n return links.filter(link => link && link.url);\n}\n\nfunction clean(value) {\n return String(value ?? '').trim();\n}\n\nfunction getYear(value) {\n const date = value ? new Date(value) : new Date();\n\n if (Number.isNaN(date.getTime())) {\n return String(new Date().getFullYear());\n }\n\n return String(date.getFullYear());\n}\n\nfunction extractGoogleFileId(url) {\n const text = clean(url);\n\n if (!text) return '';\n\n const patterns = [\n /\\/d\\/([a-zA-Z0-9_-]+)/,\n /id=([a-zA-Z0-9_-]+)/,\n /\\/file\\/d\\/([a-zA-Z0-9_-]+)/,\n /\\/presentation\\/d\\/([a-zA-Z0-9_-]+)/,\n ];\n\n for (const pattern of patterns) {\n const match = text.match(pattern);\n if (match && match[1]) return match[1];\n }\n\n return '';\n}\n\nconst rawProject = getNodeJson('Supabase - Leer proyecto');\nconst project = normalizeProjectResponse(rawProject);\n\nif (!project || !project.id) {\n throw new Error('No se encontró el proyecto en Supabase. Revisa el project_id.');\n}\n\nconst linkItems = getNodeItems('Supabase - Leer links del proyecto');\nconst links = normalizeLinksResponse(linkItems);\n\nconst proposalLinks = links.filter(link =>\n String(link.link_type || '').toLowerCase() === 'proposal'\n);\n\nif (!proposalLinks.length) {\n return [\n {\n json: {\n ok: true,\n skipped: true,\n reason: 'El proyecto no tiene links de propuesta. No se escribe en Banco Fulgencio.',\n project_id: project.id,\n },\n },\n ];\n}\n\nreturn proposalLinks.map((link, index) => {\n const proposalUrl = clean(link.url);\n const fileId = extractGoogleFileId(proposalUrl);\n const syncKey = `${project.id}::${fileId || proposalUrl}`;\n\n return {\n json: {\n skipped: false,\n\n 'NOMBRE': project.title || '',\n 'TIPO DE ACCION': 'PENDIENTE CLASIFICAR',\n 'CLIENTE': project.client || '',\n 'MARCA': project.brand || '',\n 'PAIS': project.country || '',\n 'CANAL': '',\n 'AMBIENTE DE COMPRA (RE)': '',\n 'TÁCTICA PROMOCIONAL': '',\n 'APROBADA': '',\n 'ETIQUETAS': '',\n 'AÑO': getYear(project.created_at),\n 'Enlace a la propuesta': proposalUrl,\n 'Descripcion': project.description || project.title || '',\n 'file_id': fileId,\n 'nombre_archivo': '',\n 'mime_type': '',\n 'fuente_pais': 'Tablero CDC',\n 'confianza_pais': '',\n 'requiere_revision': '',\n 'procesado_ia': 'NO',\n 'ultima_actualizacion': new Date().toISOString(),\n 'motivos_revision': '',\n 'Enlaces a propuestas ejecutadas': '',\n\n 'tablero_sync_key': syncKey,\n 'tablero_project_id': project.id,\n 'tablero_origen': 'Tablero CDC',\n },\n };\n});" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1088, + 0 + ], + "id": "ddfd7064-ad22-4a7c-aa8f-e400b132da83", + "name": "Code - Preparar filas Fulgencio" + }, + { + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "8b0c7244-a4a1-4fd9-b2eb-dc62b98fb7a3", + "leftValue": "={{ $json.skipped }}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "false", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "options": {} + }, + "type": "n8n-nodes-base.if", + "typeVersion": 2.3, + "position": [ + 1296, + 0 + ], + "id": "d6a1baa5-c035-4645-885e-d31b89a6ff2f", + "name": "IF - Tiene filas para escribir?" + }, + { + "parameters": { + "respondWith": "json", + "responseBody": "{\n \"ok\": true,\n \"message\": \"Proyecto sincronizado con Banco Fulgencio\",\n \"source\": \"Tablero CDC\"\n}", + "options": {} + }, + "type": "n8n-nodes-base.respondToWebhook", + "typeVersion": 1.5, + "position": [ + 2832, + -96 + ], + "id": "875c95aa-c12e-40a1-b64c-4da83d0a1b06", + "name": "Respond to Webhook" + }, + { + "parameters": { + "respondWith": "json", + "responseBody": "{\n \"ok\": true,\n \"skipped\": true,\n \"message\": \"Proyecto sin links de propuesta. No se escribió en Banco Fulgencio.\"\n}", + "options": {} + }, + "type": "n8n-nodes-base.respondToWebhook", + "typeVersion": 1.5, + "position": [ + 1504, + 96 + ], + "id": "3d315115-95b4-4b60-a174-2067bb5545d2", + "name": "Respond to Webhook1" + }, + { + "parameters": { + "operation": "appendOrUpdate", + "documentId": { + "__rl": true, + "value": "1iy3lC3DS3ZfShb7d26iQOrPjx9s_3B-PC07XfYew7ng", + "mode": "list", + "cachedResultName": "BANCO DE PROPUESTAS DE CDC PARA FULGENCIO FUMADO", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1iy3lC3DS3ZfShb7d26iQOrPjx9s_3B-PC07XfYew7ng/edit?usp=drivesdk" + }, + "sheetName": { + "__rl": true, + "value": "gid=0", + "mode": "list", + "cachedResultName": "propuestas", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1iy3lC3DS3ZfShb7d26iQOrPjx9s_3B-PC07XfYew7ng/edit#gid=0" + }, + "columns": { + "mappingMode": "defineBelow", + "value": { + "NOMBRE": "={{ $json[\"NOMBRE\"] }}", + "TIPO DE ACCION": "={{ $json[\"TIPO DE ACCION\"] }}", + "CLIENTE": "={{ $json[\"CLIENTE\"] }}", + "MARCA": "={{ $json[\"MARCA\"] }}", + "PAIS": "={{ $json[\"PAIS\"] }}", + "CANAL": "={{ $json[\"CANAL\"] }}", + "AMBIENTE DE COMPRA (RE)": "={{ $json[\"AMBIENTE DE COMPRA (RE)\"] }}", + "TÁCTICA PROMOCIONAL": "={{ $json[\"TÁCTICA PROMOCIONAL\"] }}", + "APROBADA": "={{ $json[\"APROBADA\"] }}", + "ETIQUETAS": "={{ $json[\"ETIQUETAS\"] }}", + "AÑO": "={{ $json[\"AÑO\"] }}", + "Enlace a la propuesta": "={{ $json[\"Enlace a la propuesta\"] }}", + "Descripcion": "={{ $json[\"Descripcion\"] }}", + "file_id": "={{ $json[\"file_id\"] }}", + "nombre_archivo": "={{ $json[\"nombre_archivo\"] }}", + "mime_type": "={{ $json[\"mime_type\"] }}", + "fuente_pais": "={{ $json[\"fuente_pais\"] }}", + "confianza_pais": "={{ $json[\"confianza_pais\"] }}", + "requiere_revision": "={{ $json[\"requiere_revision\"] }}", + "procesado_ia": "={{ $json[\"procesado_ia\"] }}", + "ultima_actualizacion": "={{ $json[\"ultima_actualizacion\"] }}", + "motivos_revision": "={{ $json[\"motivos_revision\"] }}", + "Enlaces a propuestas ejecutadas": "={{ $json[\"Enlaces a propuestas ejecutadas\"] }}", + "tablero_project_id": "={{ $json[\"tablero_project_id\"] }}", + "tablero_origen": "={{ $json[\"tablero_origen\"] }}", + "tablero_sync_key": "={{ $json[\"tablero_sync_key\"] }}" + }, + "matchingColumns": [ + "tablero_sync_key" + ], + "schema": [ + { + "id": "NOMBRE", + "displayName": "NOMBRE", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "TIPO DE ACCION", + "displayName": "TIPO DE ACCION", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "CLIENTE", + "displayName": "CLIENTE", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "MARCA", + "displayName": "MARCA", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "PAIS", + "displayName": "PAIS", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "CANAL", + "displayName": "CANAL", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "AMBIENTE DE COMPRA (RE)", + "displayName": "AMBIENTE DE COMPRA (RE)", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "TÁCTICA PROMOCIONAL", + "displayName": "TÁCTICA PROMOCIONAL", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "APROBADA", + "displayName": "APROBADA", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "ETIQUETAS", + "displayName": "ETIQUETAS", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "AÑO", + "displayName": "AÑO", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "Enlace a la propuesta", + "displayName": "Enlace a la propuesta", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "Descripcion", + "displayName": "Descripcion", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "file_id", + "displayName": "file_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "nombre_archivo", + "displayName": "nombre_archivo", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "mime_type", + "displayName": "mime_type", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "fuente_pais", + "displayName": "fuente_pais", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "confianza_pais", + "displayName": "confianza_pais", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "requiere_revision", + "displayName": "requiere_revision", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "procesado_ia", + "displayName": "procesado_ia", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "ultima_actualizacion", + "displayName": "ultima_actualizacion", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "motivos_revision", + "displayName": "motivos_revision", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "Enlaces a propuestas ejecutadas", + "displayName": "Enlaces a propuestas ejecutadas", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "tablero_sync_key", + "displayName": "tablero_sync_key", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "tablero_project_id", + "displayName": "tablero_project_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "tablero_origen", + "displayName": "tablero_origen", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + } + ], + "attemptToConvertTypes": false, + "convertFieldsToString": false + }, + "options": {} + }, + "type": "n8n-nodes-base.googleSheets", + "typeVersion": 4.7, + "position": [ + 2624, + -96 + ], + "id": "f285672d-8c71-4ebd-87ec-a8b83c4e1ce6", + "name": "Google Sheets - Upsert Banco Fulgencio", + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + } + }, + { + "parameters": { + "jsCode": "function clean(value) {\n return String(value ?? '').trim().replace(/\\s+/g, ' ');\n}\n\nconst item = $input.first().json || {};\n\nconst nombre = clean(item.NOMBRE);\nconst cliente = clean(item.CLIENTE);\nconst marca = clean(item.MARCA);\nconst pais = clean(item.PAIS);\nconst enlace = clean(item['Enlace a la propuesta']);\n\nreturn [\n {\n json: {\n ...item,\n\n // Campos compatibles con el flujo viejo de Fulgencio\n nombre_archivo: nombre || item.nombre_archivo || '',\n nombre_limpio: nombre || item.nombre_archivo || '',\n pais: pais || '',\n cliente_tablero: cliente || '',\n marca_tablero: marca || '',\n link_drive: enlace || '',\n mime_type: item.mime_type || '',\n },\n },\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1552, + -112 + ], + "id": "bc209351-3c89-49fe-b891-3fd8d1fc5c4d", + "name": "Code - Preparar entrada IA Tablero" + }, + { + "parameters": { + "text": "==Nombre del proyecto/propuesta: {{$json.nombre_limpio}}\nCliente indicado en Tablero CDC: {{$json.cliente_tablero}}\nMarca indicada en Tablero CDC: {{$json.marca_tablero}}\nPaís indicado en Tablero CDC: {{$json.pais}}\nTipo de archivo: {{$json.mime_type}}\nLink de propuesta: {{$json.link_drive}}", + "attributes": { + "attributes": [ + { + "name": "nombre_propuesta", + "description": "Nombre limpio o comercial de la propuesta." + }, + { + "name": "tipo_accion", + "description": "Tipo de acción o formato de propuesta. Ejemplos: campaña, evento, mueble, exhibidor, diseño gráfico, activación PDV, material POP, uniforme, promoción, stand. Si no se puede determinar, usar PENDIENTE." + }, + { + "name": "canal", + "description": "Canal o lugar donde aplica la propuesta. Ejemplos: moderno, tradicional, online, PDV, no aplica. Si no se puede determinar, usar PENDIENTE." + }, + { + "name": "tags", + "description": "Palabras clave separadas por comas para facilitar búsqueda. Ejemplos: campaña, corporativo, exhibidor, punto de venta, juegos, uniforme, material POP." + }, + { + "name": "descripcion", + "description": "Párrafo profesional de 35 a 70 palabras. Debe iniciar preferiblemente con “La presentación muestra...”, “La pieza muestra...” o “La propuesta corresponde a...”. Debe mencionar cliente/marca, tipo de acción, concepto o tema principal y objetivo. No inventar detalles." + }, + { + "name": "tactica_promocional", + "description": "Táctica promocional principal. Ejemplos: DEGUSTACIÓN, RULETA, SAMPLING - MUESTREO, PLINKO, WHATSAPP, LANDING PAGE, POP, PHOTOBOOTH, CANJE, SORTEO, PREMIOS INSTANTÁNEOS, JUEGO DIGITAL, TRIVIA, EXHIBICIÓN, IMPULSO o PENDIENTE." + } + ] + }, + "options": { + "systemPromptTemplate": "Analiza la siguiente propuesta usando la información disponible desde Tablero CDC.\n\nIMPORTANTE:\n- El país viene desde Tablero CDC. No lo cambies.\n- El cliente y la marca vienen desde Tablero CDC. Úsalos como referencia principal.\n- No determines APROBADA. Ese campo se guardará como PENDIENTE DE APROBACION.\n- No determines AMBIENTE DE COMPRA (RE). Ese campo se guardará como PENDIENTE.\n- Si no puedes determinar un campo con claridad, usa PENDIENTE.\n- No inventes detalles que no estén claros a partir del nombre, cliente, marca, país o link.\n- Extrae metadata para alimentar el banco de propuestas de Fulgencio Fumado.\n\nDevuelve estos campos:\n- nombre_propuesta\n- tipo_accion\n- canal\n- tags\n- descripcion\n- tactica_promocional\n\nReglas:\n1. tipo_accion puede inferirse del nombre y contexto. Ejemplos: campaña, evento, mueble, exhibidor, activación PDV, diseño gráfico, promoción, uniforme, stand, material POP.\n2. canal debe ser MODERNO, TRADICIONAL, ONLINE, PDV, NO APLICA o PENDIENTE cuando sea posible.\n3. tags debe ser una lista corta de palabras clave separadas por comas.\n4. descripcion debe ser un párrafo profesional de 35 a 70 palabras, similar al estilo usado en el banco actual.\n5. tactica_promocional debe ser una opción clara como DEGUSTACIÓN, RULETA, SAMPLING - MUESTREO, PLINKO, WHATSAPP, LANDING PAGE, POP, PHOTOBOOTH, CANJE, SORTEO, PREMIOS INSTANTÁNEOS, JUEGO DIGITAL, TRIVIA, EXHIBICIÓN, IMPULSO, o PENDIENTE." + } + }, + "type": "@n8n/n8n-nodes-langchain.informationExtractor", + "typeVersion": 1.2, + "position": [ + 2064, + -96 + ], + "id": "28887e16-b735-4e11-8fe2-4e3270876c85", + "name": "Gemini - Extraer metadata Tablero" + }, + { + "parameters": { + "modelName": "models/gemini-2.5-pro", + "options": {} + }, + "type": "@n8n/n8n-nodes-langchain.lmChatGoogleGemini", + "typeVersion": 1, + "position": [ + 2080, + 144 + ], + "id": "4b4e6b14-50fc-4700-ad33-dcfc8a489770", + "name": "Gemini - Extraer metadata1", + "credentials": { + "googlePalmApi": { + "id": "jvsXYwL6IOoY2DBU", + "name": "Isaac - Gemini Api Pago" + } + } + }, + { + "parameters": { + "jsCode": "const original = $('Code - Preparar entrada IA Tablero').item.json || {};\nconst incoming = $input.first().json || {};\nconst ai = incoming.output || incoming;\n\nfunction clean(value) {\n if (Array.isArray(value)) return value.map(clean).filter(Boolean).join(', ');\n return String(value ?? '').trim().replace(/\\s+/g, ' ');\n}\n\nfunction removeAccents(value) {\n return clean(value)\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '');\n}\n\nfunction upper(value) {\n return removeAccents(value).toUpperCase();\n}\n\nfunction splitMulti(value) {\n return clean(value)\n .split(',')\n .map(v => upper(v))\n .map(v => v.trim())\n .filter(Boolean);\n}\n\nfunction unique(values) {\n return [...new Set(values.filter(Boolean))];\n}\n\nfunction joinMulti(values) {\n return unique(values).join(', ');\n}\n\nfunction normalizeTipoAccion(value) {\n const v = upper(value);\n\n const synonyms = {\n 'ACTIVACION PDV': 'ACTIVACION EN PDV',\n 'ACTIVACION EN PUNTO DE VENTA': 'ACTIVACION EN PDV',\n 'ACTIVACION PUNTO DE VENTA': 'ACTIVACION EN PDV',\n 'PUNTO DE VENTA': 'ACTIVACION EN PDV',\n 'PDV': 'ACTIVACION EN PDV',\n 'DISEÑO GRAFICO': 'DISENO GRAFICO',\n 'DISEÑO GRÁFICO': 'DISENO GRAFICO',\n 'DISENO GRAFICO': 'DISENO GRAFICO',\n 'CAMPAÑA': 'CAMPAÑA',\n 'CAMPANA': 'CAMPAÑA',\n 'PROMOCION': 'PROMOCION',\n 'PROMOCIÓN': 'PROMOCION',\n 'MUEBLE': 'MUEBLE',\n 'EXHIBIDOR': 'MUEBLE',\n 'STAND': 'STAND',\n 'EVENTO': 'EVENTO',\n 'UNIFORME': 'UNIFORME',\n 'MATERIAL POP': 'DISENO GRAFICO',\n 'POP': 'DISENO GRAFICO'\n };\n\n return synonyms[v] || v;\n}\n\nfunction normalizeCanal(value) {\n const v = upper(value);\n\n const synonyms = {\n 'PUNTO DE VENTA': 'PDV',\n 'PDV': 'PDV',\n 'CANAL MODERNO': 'MODERNO',\n 'MODERNO': 'MODERNO',\n 'CANAL TRADICIONAL': 'TRADICIONAL',\n 'TRADICIONAL': 'TRADICIONAL',\n 'DIGITAL': 'ONLINE',\n 'ONLINE': 'ONLINE',\n 'NO APLICA': 'NO APLICA',\n 'N/A': 'NO APLICA'\n };\n\n return synonyms[v] || v;\n}\n\nfunction normalizeTactica(value) {\n const v = upper(value);\n\n const synonyms = {\n 'SAMPLING': 'SAMPLING - MUESTREO',\n 'MUESTREO': 'SAMPLING - MUESTREO',\n 'DEGUSTACION': 'DEGUSTACIÓN',\n 'DEGUSTACIÓN': 'DEGUSTACIÓN',\n 'RULETA DIGITAL': 'RULETA',\n 'RULETA': 'RULETA',\n 'PLINKO': 'PLINKO',\n 'WHATSAPP': 'WHATSAPP',\n 'LANDING': 'LANDING PAGE',\n 'LANDING PAGE': 'LANDING PAGE',\n 'MATERIAL POP': 'POP',\n 'POP': 'POP',\n 'JUEGO DIGITAL': 'JUEGO DIGITAL',\n 'JUEGOS DIGITALES': 'JUEGO DIGITAL',\n 'EXHIBICION': 'EXHIBICIÓN',\n 'EXHIBICIÓN': 'EXHIBICIÓN',\n 'IMPULSO': 'IMPULSO'\n };\n\n return synonyms[v] || v;\n}\n\nfunction normalizeEtiqueta(value) {\n const v = upper(value);\n\n const synonyms = {\n 'ACTIVACION': 'ACTIVACION',\n 'ACTIVACIÓN': 'ACTIVACION',\n 'EXHIBICION': 'EXHIBICION',\n 'EXHIBICIÓN': 'EXHIBICION',\n 'EXHIBIDOR': 'EXHIBIDOR',\n 'DEGUSTACION': 'DEGUSTACION',\n 'DEGUSTACIÓN': 'DEGUSTACION',\n 'PROMOCION': 'PROMOCIÓN',\n 'PROMOCIÓN': 'PROMOCIÓN',\n 'DISENO': 'DISEÑO',\n 'DISEÑO': 'DISEÑO',\n 'CAMPANA': 'CAMPAÑA',\n 'CAMPAÑA': 'CAMPAÑA',\n 'JUEGO DIGITAL': 'JUEGOS DIGITALES',\n 'JUEGOS DIGITALES': 'JUEGOS DIGITALES',\n 'PUNTO DE VENTA': 'PUNTO DE VENTA',\n 'PDV': 'PUNTO DE VENTA',\n 'F1': 'JUEGOS',\n 'SIMULADOR': 'JUEGOS DIGITALES',\n 'EXPERIENCIA': 'ACTIVIDADES',\n 'CORPORATIVO': 'CORPORATIVO',\n 'UNIFORME': 'UNIFORMES',\n 'UNIFORMES': 'UNIFORMES',\n 'POP': 'PUNTO DE VENTA',\n 'MATERIAL POP': 'PUNTO DE VENTA'\n };\n\n return synonyms[v] || v;\n}\n\nfunction normalizeOne(value, allowedList, fallback = 'PENDIENTE', normalizer = upper) {\n const raw = clean(value);\n if (!raw) return fallback;\n\n const mapped = normalizer(raw);\n return allowedList.includes(mapped) ? mapped : fallback;\n}\n\nfunction normalizeMulti(value, allowedList, fallback = 'PENDIENTE', unknownValue = 'OTRO', normalizer = upper) {\n const parts = splitMulti(value);\n if (!parts.length) return fallback;\n\n const result = [];\n\n for (const part of parts) {\n const mapped = normalizer(part);\n const expanded = String(mapped).split(',').map(v => v.trim()).filter(Boolean);\n\n for (const val of expanded) {\n if (allowedList.includes(val)) {\n result.push(val);\n } else {\n result.push(unknownValue);\n }\n }\n }\n\n const finalValues = unique(result);\n if (!finalValues.length) return fallback;\n\n return joinMulti(finalValues);\n}\n\nconst ALLOWED_TIPO_ACCION = [\n 'ACTIVACION EN PDV',\n 'IMPULSO DE VENTA',\n 'ACTIVACION FUERA DE PDV',\n 'EVENTO',\n 'KICK OFF',\n 'FERIA',\n 'STAND',\n 'MUEBLE',\n 'DISENO GRAFICO',\n 'DIGITAL',\n 'PUBLICIDAD',\n 'TUTORIAL',\n 'INFORMATIVA',\n 'LOGOTIPO',\n 'PROMOCION',\n 'UNIFORME',\n 'ACTIVACIÓN',\n 'INAUGURACIÓN',\n 'PARQUE',\n 'TROFEO',\n 'ANAQUEL',\n 'FOOD TRUCK',\n 'EMPAQUES',\n 'CAMPAÑA',\n 'QUICK COUNTER',\n 'DISEÑO',\n 'PPT',\n 'PENDIENTE',\n 'OTRO'\n];\n\nconst ALLOWED_CANAL = [\n 'MODERNO',\n 'TRADICIONAL',\n 'ONLINE',\n 'NO APLICA',\n 'PDV',\n 'PENDIENTE'\n];\n\nconst ALLOWED_TACTICA = [\n 'DEGUSTACIÓN',\n 'RULETA',\n 'SAMPLING - MUESTREO',\n 'PLINKO',\n 'WHATSAPP',\n 'LANDING PAGE',\n 'POP',\n 'PHOTOBOOTH',\n 'CANJE',\n 'SORTEO',\n 'PREMIOS INSTANTÁNEOS',\n 'JUEGO DIGITAL',\n 'TRIVIA',\n 'EXHIBICIÓN',\n 'IMPULSO',\n 'PENDIENTE',\n 'OTRO'\n];\n\nconst ALLOWED_ETIQUETAS = [\n 'ACTIVACION',\n 'ACTIVIDADES',\n 'BANDEJA',\n 'BTS',\n 'CORPORATIVO',\n 'DEGUSTACION',\n 'EXHIBICION',\n 'EXHIBIDOR',\n 'FUERZA DE VENTAS',\n 'JUEGOS',\n 'PREMIOS',\n 'PUNTO DE VENTA',\n 'REFERENCIAS',\n 'REUNIÓN',\n 'RULETA',\n 'SAMPLING',\n 'HALLOWEEN',\n 'LUCES NEON',\n 'INFORMATIVA',\n 'CARRITO DEGUSTACIÓN',\n 'RED SOCIAL/INFLUENCER',\n 'UNIFORMES',\n 'REGALIAS',\n 'STAND',\n 'CAPACITACIÓN',\n 'NAVIDAD',\n 'DÍA DE LAS MADRES',\n 'INDEPENDENCIA',\n 'LOGOTIPO',\n 'QUICK COUNTER',\n 'PPT',\n 'TEMPLATES',\n 'DISEÑO',\n 'PROMOCIÓN',\n 'MODERNO',\n 'MAYORISTA',\n 'PULPERÍAS',\n 'JUEGOS DIGITALES',\n 'WHATSAPP',\n 'CAMPAÑA',\n 'CONCURSO',\n 'REGALOS CORPORATIVOS',\n 'PENDIENTE',\n 'OTRO'\n];\n\nlet nombrePropuesta = clean(ai.nombre_propuesta || original.NOMBRE || original.nombre_limpio || '');\nif (!nombrePropuesta) nombrePropuesta = 'PENDIENTE';\n\nlet tipoAccion = normalizeMulti(\n ai.tipo_accion,\n ALLOWED_TIPO_ACCION,\n 'PENDIENTE',\n 'OTRO',\n normalizeTipoAccion\n);\n\nconst canal = normalizeOne(\n ai.canal,\n ALLOWED_CANAL,\n 'PENDIENTE',\n normalizeCanal\n);\n\nconst tacticaPromocional = normalizeMulti(\n ai.tactica_promocional,\n ALLOWED_TACTICA,\n 'PENDIENTE',\n 'OTRO',\n normalizeTactica\n);\n\nlet etiquetas = normalizeMulti(\n ai.tags,\n ALLOWED_ETIQUETAS,\n 'PENDIENTE',\n 'OTRO',\n normalizeEtiqueta\n);\n\nif (etiquetas.includes('OTRO') && etiquetas !== 'OTRO') {\n etiquetas = etiquetas\n .split(',')\n .map(v => v.trim())\n .filter(v => v !== 'OTRO')\n .join(', ');\n}\n\nconst ambienteCompra = 'PENDIENTE';\nconst aprobada = 'PENDIENTE DE APROBACION';\n\nlet descripcion = clean(ai.descripcion);\nif (!descripcion) {\n descripcion = clean(original.Descripcion || original.NOMBRE || '');\n}\n\nlet requiereRevision = false;\nconst revisionReasons = [];\n\nif (tipoAccion.includes('OTRO') || tipoAccion === 'PENDIENTE') {\n requiereRevision = true;\n revisionReasons.push('tipo_accion_revisar');\n}\n\nif (canal === 'PENDIENTE') {\n requiereRevision = true;\n revisionReasons.push('canal_revisar');\n}\n\nif (tacticaPromocional.includes('OTRO') || tacticaPromocional === 'PENDIENTE') {\n requiereRevision = true;\n revisionReasons.push('tactica_revisar');\n}\n\nif (etiquetas.includes('OTRO') || etiquetas === 'PENDIENTE') {\n requiereRevision = true;\n revisionReasons.push('etiquetas_revisar');\n}\n\nreturn [\n {\n json: {\n NOMBRE: nombrePropuesta,\n 'TIPO DE ACCION': tipoAccion,\n CLIENTE: original.CLIENTE || '',\n MARCA: original.MARCA || '',\n PAIS: original.PAIS || '',\n CANAL: canal,\n 'AMBIENTE DE COMPRA (RE)': ambienteCompra,\n 'TÁCTICA PROMOCIONAL': tacticaPromocional,\n APROBADA: aprobada,\n ETIQUETAS: etiquetas,\n 'AÑO': original['AÑO'] || '',\n 'Enlace a la propuesta': original['Enlace a la propuesta'] || '',\n Descripcion: descripcion,\n\n file_id: original.file_id || '',\n nombre_archivo: original.nombre_archivo || original.NOMBRE || '',\n mime_type: original.mime_type || '',\n fuente_pais: original.fuente_pais || 'Tablero CDC',\n confianza_pais: original.confianza_pais || '',\n requiere_revision: requiereRevision,\n motivos_revision: revisionReasons.join(', '),\n procesado_ia: true,\n ultima_actualizacion: new Date().toISOString(),\n 'Enlaces a propuestas ejecutadas': original['Enlaces a propuestas ejecutadas'] || '',\n\n tablero_sync_key: original.tablero_sync_key || '',\n tablero_project_id: original.tablero_project_id || '',\n tablero_origen: original.tablero_origen || 'Tablero CDC',\n },\n },\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 2416, + -96 + ], + "id": "a183d0af-b313-49ce-9f00-e619a4d576d2", + "name": "Code - Normalizar respuesta IA Tablero" + }, + { + "parameters": { + "content": "## ENTRADA DESDE TABLERO CDC\n\nEste flujo recibe un project_id desde la app Tablero CDC.\n\n1. Recibe el webhook.\n2. Extrae y valida el project_id.\n3. Busca el proyecto en Supabase.\n4. Busca los links asociados al proyecto.\n\nSi falta project_id o el proyecto no existe, el flujo debe detenerse con error.\n", + "height": 416, + "width": 816 + }, + "type": "n8n-nodes-base.stickyNote", + "typeVersion": 1, + "position": [ + -64, + -240 + ], + "id": "2e6ca1d3-9ec6-4e10-b595-f7d89e741d92", + "name": "Sticky Note" + }, + { + "parameters": { + "content": "## PREPARACIÓN PARA BANCO FULGENCIO\n\nEste bloque convierte el proyecto del Tablero en una o varias filas para el Banco de Propuestas de Fulgencio.\n\nRegla principal:\n\n* Solo se escriben links tipo propuesta.\n* Si el proyecto no tiene links de propuesta, no se escribe nada y se responde como skipped.\n* Cada propuesta genera una tablero_sync_key única para evitar duplicados.\n", + "height": 592, + "width": 752, + "color": 5 + }, + "type": "n8n-nodes-base.stickyNote", + "typeVersion": 1, + "position": [ + 992, + -336 + ], + "id": "27212676-c05e-4315-90e8-2372b0122e4f", + "name": "Sticky Note1" + }, + { + "parameters": { + "content": "## IA + ESCRITURA EN BANCO FULGENCIO\n\nGemini analiza la información del proyecto y extrae metadata útil para búsqueda:\ntipo de acción, canal, etiquetas, descripción y táctica promocional.\n\nLuego el flujo normaliza los valores para respetar listas permitidas y hace append/update en Google Sheets usando tablero_sync_key.\n\nResultado final:\n\n* Proyecto sincronizado con Banco Fulgencio.\n* Si algo queda dudoso, se marca requiere_revision.\n", + "height": 656, + "width": 1104, + "color": 4 + }, + "type": "n8n-nodes-base.stickyNote", + "typeVersion": 1, + "position": [ + 1936, + -336 + ], + "id": "9e829a95-7b23-42f0-89a9-eb1450fff26c", + "name": "Sticky Note2" + } + ], + "pinData": {}, + "connections": { + "Webhook": { + "main": [ + [ + { + "node": "Code - Preparar project_id", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar project_id": { + "main": [ + [ + { + "node": "Supabase - Leer proyecto", + "type": "main", + "index": 0 + } + ] + ] + }, + "Supabase - Leer proyecto": { + "main": [ + [ + { + "node": "Supabase - Leer links del proyecto", + "type": "main", + "index": 0 + } + ] + ] + }, + "Supabase - Leer links del proyecto": { + "main": [ + [ + { + "node": "Code - Preparar filas Fulgencio", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar filas Fulgencio": { + "main": [ + [ + { + "node": "IF - Tiene filas para escribir?", + "type": "main", + "index": 0 + } + ] + ] + }, + "IF - Tiene filas para escribir?": { + "main": [ + [ + { + "node": "Code - Preparar entrada IA Tablero", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Respond to Webhook1", + "type": "main", + "index": 0 + } + ] + ] + }, + "Google Sheets - Upsert Banco Fulgencio": { + "main": [ + [ + { + "node": "Respond to Webhook", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar entrada IA Tablero": { + "main": [ + [ + { + "node": "Gemini - Extraer metadata Tablero", + "type": "main", + "index": 0 + } + ] + ] + }, + "Gemini - Extraer metadata1": { + "ai_languageModel": [ + [ + { + "node": "Gemini - Extraer metadata Tablero", + "type": "ai_languageModel", + "index": 0 + } + ] + ] + }, + "Gemini - Extraer metadata Tablero": { + "main": [ + [ + { + "node": "Code - Normalizar respuesta IA Tablero", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Normalizar respuesta IA Tablero": { + "main": [ + [ + { + "node": "Google Sheets - Upsert Banco Fulgencio", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": true, + "settings": { + "executionOrder": "v1", + "binaryMode": "separate", + "availableInMCP": true, + "timeSavedMode": "fixed", + "errorWorkflow": "puF4LUczoSz3hcek", + "timezone": "America/Santo_Domingo", + "callerPolicy": "workflowsFromSameOwner" + }, + "versionId": "229bf234-9e68-455e-a84d-a47983640b83", + "meta": { + "templateCredsSetupCompleted": true, + "instanceId": "b4b77b17af092830e794eef639ce2f6d7daccf7eddc075060b03b3b6545aac70" + }, + "id": "aJH5uWZx3lUnNBbx", + "tags": [] +} \ No newline at end of file diff --git a/Tablero CDC - Sync Proyecto a Banco Fulgencio.json b/Tablero CDC - Sync Proyecto a Banco Fulgencio.json new file mode 100644 index 0000000..41c3af9 --- /dev/null +++ b/Tablero CDC - Sync Proyecto a Banco Fulgencio.json @@ -0,0 +1,610 @@ +{ + "name": "Tablero CDC - Sync Proyecto a Banco Fulgencio", + "nodes": [ + { + "parameters": { + "httpMethod": "POST", + "path": "tablero-cdc-sync-fulgencio", + "responseMode": "responseNode", + "options": {} + }, + "type": "n8n-nodes-base.webhook", + "typeVersion": 2.1, + "position": [ + -272, + 96 + ], + "id": "5fbd25cf-6c2d-4364-a10e-91954b61256f", + "name": "Webhook", + "webhookId": "b9c9551b-86ee-4f3d-bbe2-12e93eef4b35" + }, + { + "parameters": { + "jsCode": "const input = $json || {};\nconst body = input.body || input;\n\nconst projectId =\n body.project_id ||\n body.projectId ||\n body.id ||\n '';\n\nconst action =\n String(body.action || 'sync').trim().toLowerCase();\n\nif (!projectId) {\n throw new Error('Falta project_id en el payload del webhook');\n}\n\nreturn [\n {\n json: {\n action,\n project_id: projectId,\n received_at: new Date().toISOString(),\n },\n },\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -64, + 96 + ], + "id": "61c5d83f-33d1-45d8-bb6a-4f657461f3c1", + "name": "Code - Preparar project_id" + }, + { + "parameters": { + "url": "=https://dbit.digitalcompass.agency/rest/v1/tablero_cdc_projects?id=eq.{{ $('Code - Preparar project_id').first().json.project_id }}&select=*", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "apikey", + "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q" + }, + { + "name": "Authorization", + "value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q" + }, + { + "name": "Accept", + "value": " application/json" + }, + { + "name": "Content-Type", + "value": "application/json" + } + ] + }, + "options": {} + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.4, + "position": [ + 144, + 96 + ], + "id": "007cdb0b-ab8e-4558-8ce0-b4cf238b2ef9", + "name": "Supabase - Leer proyecto" + }, + { + "parameters": { + "url": "=https://dbit.digitalcompass.agency/rest/v1/tablero_cdc_project_links?project_id=eq.{{ $('Code - Preparar project_id').first().json.project_id }}&select=*&order=created_at.asc", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "apikey", + "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q" + }, + { + "name": "Authorization", + "value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q" + }, + { + "name": "Accept", + "value": " application/json" + }, + { + "name": "Content-Type", + "value": "application/json" + } + ] + }, + "options": {} + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.4, + "position": [ + 320, + 96 + ], + "id": "ab47b077-7d17-4145-9a19-c0b9134a7931", + "name": "Supabase - Leer links del proyecto", + "alwaysOutputData": true + }, + { + "parameters": { + "jsCode": "function getNodeJson(nodeName) {\n try {\n return $(nodeName).first().json;\n } catch (error) {\n return {};\n }\n}\n\nfunction getNodeItems(nodeName) {\n try {\n return $(nodeName).all();\n } catch (error) {\n return [];\n }\n}\n\nfunction normalizeProjectResponse(value) {\n if (Array.isArray(value)) return value[0] || null;\n return value || null;\n}\n\nfunction normalizeLinksResponse(items) {\n const links = [];\n\n for (const item of items) {\n const json = item.json;\n\n if (Array.isArray(json)) {\n links.push(...json);\n } else if (json && typeof json === 'object') {\n links.push(json);\n }\n }\n\n return links.filter(link => link && link.url);\n}\n\nfunction clean(value) {\n return String(value ?? '').trim();\n}\n\nfunction getYear(value) {\n const date = value ? new Date(value) : new Date();\n\n if (Number.isNaN(date.getTime())) {\n return String(new Date().getFullYear());\n }\n\n return String(date.getFullYear());\n}\n\nfunction extractGoogleFileId(url) {\n const text = clean(url);\n\n if (!text) return '';\n\n const patterns = [\n /\\/d\\/([a-zA-Z0-9_-]+)/,\n /id=([a-zA-Z0-9_-]+)/,\n /\\/file\\/d\\/([a-zA-Z0-9_-]+)/,\n /\\/presentation\\/d\\/([a-zA-Z0-9_-]+)/,\n ];\n\n for (const pattern of patterns) {\n const match = text.match(pattern);\n if (match && match[1]) return match[1];\n }\n\n return '';\n}\n\nconst rawProject = getNodeJson('Supabase - Leer proyecto');\nconst project = normalizeProjectResponse(rawProject);\n\nif (!project || !project.id) {\n throw new Error('No se encontró el proyecto en Supabase. Revisa el project_id.');\n}\n\nconst linkItems = getNodeItems('Supabase - Leer links del proyecto');\nconst links = normalizeLinksResponse(linkItems);\n\nconst proposalLinks = links.filter(link =>\n String(link.link_type || '').toLowerCase() === 'proposal'\n);\n\nif (!proposalLinks.length) {\n return [\n {\n json: {\n ok: true,\n skipped: true,\n reason: 'El proyecto no tiene links de propuesta. No se escribe en Banco Fulgencio.',\n project_id: project.id,\n },\n },\n ];\n}\n\nreturn proposalLinks.map((link, index) => {\n const proposalUrl = clean(link.url);\n const fileId = extractGoogleFileId(proposalUrl);\n const syncKey = `${project.id}::${fileId || proposalUrl}`;\n\n return {\n json: {\n skipped: false,\n\n 'NOMBRE': project.title || '',\n 'TIPO DE ACCION': 'PENDIENTE CLASIFICAR',\n 'CLIENTE': project.client || '',\n 'MARCA': project.brand || '',\n 'PAIS': project.country || '',\n 'CANAL': '',\n 'AMBIENTE DE COMPRA (RE)': '',\n 'TÁCTICA PROMOCIONAL': '',\n 'APROBADA': '',\n 'ETIQUETAS': '',\n 'AÑO': getYear(project.created_at),\n 'Enlace a la propuesta': proposalUrl,\n 'Descripcion': project.description || project.title || '',\n 'file_id': fileId,\n 'nombre_archivo': '',\n 'mime_type': '',\n 'fuente_pais': 'Tablero CDC',\n 'confianza_pais': '',\n 'requiere_revision': '',\n 'procesado_ia': 'NO',\n 'ultima_actualizacion': new Date().toISOString(),\n 'motivos_revision': '',\n 'Enlaces a propuestas ejecutadas': '',\n\n 'tablero_sync_key': syncKey,\n 'tablero_project_id': project.id,\n 'tablero_origen': 'Tablero CDC',\n },\n };\n});" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 528, + 96 + ], + "id": "4a37a2a6-3138-455b-8806-288d852873a6", + "name": "Code - Preparar filas Fulgencio" + }, + { + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "8b0c7244-a4a1-4fd9-b2eb-dc62b98fb7a3", + "leftValue": "={{ $json.skipped }}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "false", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "options": {} + }, + "type": "n8n-nodes-base.if", + "typeVersion": 2.3, + "position": [ + 736, + 96 + ], + "id": "5b14f1ea-8203-4daf-8f14-b39da2a7dc97", + "name": "IF - Tiene filas para escribir?" + }, + { + "parameters": { + "respondWith": "json", + "responseBody": "{\n \"ok\": true,\n \"message\": \"Proyecto sincronizado con Banco Fulgencio\",\n \"source\": \"Tablero CDC\"\n}", + "options": {} + }, + "type": "n8n-nodes-base.respondToWebhook", + "typeVersion": 1.5, + "position": [ + 1248, + 0 + ], + "id": "c221409e-9777-4453-afa1-ad8bc7107910", + "name": "Respond to Webhook" + }, + { + "parameters": { + "respondWith": "json", + "responseBody": "{\n \"ok\": true,\n \"skipped\": true,\n \"message\": \"Proyecto sin links de propuesta. No se escribió en Banco Fulgencio.\"\n}", + "options": {} + }, + "type": "n8n-nodes-base.respondToWebhook", + "typeVersion": 1.5, + "position": [ + 944, + 192 + ], + "id": "363e5ac4-43be-4a9e-afd0-923bec81a017", + "name": "Respond to Webhook1" + }, + { + "parameters": { + "operation": "appendOrUpdate", + "documentId": { + "__rl": true, + "value": "1iy3lC3DS3ZfShb7d26iQOrPjx9s_3B-PC07XfYew7ng", + "mode": "list", + "cachedResultName": "BANCO DE PROPUESTAS DE CDC PARA FULGENCIO FUMADO", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1iy3lC3DS3ZfShb7d26iQOrPjx9s_3B-PC07XfYew7ng/edit?usp=drivesdk" + }, + "sheetName": { + "__rl": true, + "value": "gid=0", + "mode": "list", + "cachedResultName": "propuestas", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1iy3lC3DS3ZfShb7d26iQOrPjx9s_3B-PC07XfYew7ng/edit#gid=0" + }, + "columns": { + "mappingMode": "defineBelow", + "value": { + "NOMBRE": "={{ $json[\"NOMBRE\"] }}", + "TIPO DE ACCION": "={{ $json[\"TIPO DE ACCION\"] }}", + "CLIENTE": "={{ $json[\"CLIENTE\"] }}", + "MARCA": "={{ $json[\"MARCA\"] }}", + "PAIS": "={{ $json[\"PAIS\"] }}", + "CANAL": "={{ $json[\"CANAL\"] }}", + "AMBIENTE DE COMPRA (RE)": "={{ $json[\"AMBIENTE DE COMPRA (RE)\"] }}", + "TÁCTICA PROMOCIONAL": "={{ $json[\"TÁCTICA PROMOCIONAL\"] }}", + "APROBADA": "={{ $json[\"APROBADA\"] }}", + "ETIQUETAS": "={{ $json[\"ETIQUETAS\"] }}", + "AÑO": "={{ $json[\"AÑO\"] }}", + "Enlace a la propuesta": "={{ $json[\"Enlace a la propuesta\"] }}", + "Descripcion": "={{ $json[\"Descripcion\"] }}", + "file_id": "={{ $json[\"file_id\"] }}", + "nombre_archivo": "={{ $json[\"nombre_archivo\"] }}", + "mime_type": "={{ $json[\"mime_type\"] }}", + "fuente_pais": "={{ $json[\"fuente_pais\"] }}", + "confianza_pais": "={{ $json[\"confianza_pais\"] }}", + "requiere_revision": "={{ $json[\"requiere_revision\"] }}", + "procesado_ia": "={{ $json[\"procesado_ia\"] }}", + "ultima_actualizacion": "={{ $json[\"ultima_actualizacion\"] }}", + "motivos_revision": "={{ $json[\"motivos_revision\"] }}", + "Enlaces a propuestas ejecutadas": "={{ $json[\"Enlaces a propuestas ejecutadas\"] }}", + "tablero_project_id": "={{ $json[\"tablero_project_id\"] }}", + "tablero_origen": "={{ $json[\"tablero_origen\"] }}", + "tablero_sync_key": "={{ $json[\"tablero_sync_key\"] }}" + }, + "matchingColumns": [ + "tablero_sync_key" + ], + "schema": [ + { + "id": "NOMBRE", + "displayName": "NOMBRE", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "TIPO DE ACCION", + "displayName": "TIPO DE ACCION", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "CLIENTE", + "displayName": "CLIENTE", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "MARCA", + "displayName": "MARCA", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "PAIS", + "displayName": "PAIS", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "CANAL", + "displayName": "CANAL", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "AMBIENTE DE COMPRA (RE)", + "displayName": "AMBIENTE DE COMPRA (RE)", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "TÁCTICA PROMOCIONAL", + "displayName": "TÁCTICA PROMOCIONAL", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "APROBADA", + "displayName": "APROBADA", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "ETIQUETAS", + "displayName": "ETIQUETAS", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "AÑO", + "displayName": "AÑO", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "Enlace a la propuesta", + "displayName": "Enlace a la propuesta", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "Descripcion", + "displayName": "Descripcion", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "file_id", + "displayName": "file_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "nombre_archivo", + "displayName": "nombre_archivo", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "mime_type", + "displayName": "mime_type", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "fuente_pais", + "displayName": "fuente_pais", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "confianza_pais", + "displayName": "confianza_pais", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "requiere_revision", + "displayName": "requiere_revision", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "procesado_ia", + "displayName": "procesado_ia", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "ultima_actualizacion", + "displayName": "ultima_actualizacion", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "motivos_revision", + "displayName": "motivos_revision", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "Enlaces a propuestas ejecutadas", + "displayName": "Enlaces a propuestas ejecutadas", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "tablero_sync_key", + "displayName": "tablero_sync_key", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "tablero_project_id", + "displayName": "tablero_project_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "tablero_origen", + "displayName": "tablero_origen", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + } + ], + "attemptToConvertTypes": false, + "convertFieldsToString": false + }, + "options": {} + }, + "type": "n8n-nodes-base.googleSheets", + "typeVersion": 4.7, + "position": [ + 1040, + 0 + ], + "id": "f3bee0f1-50da-4908-923f-51b17a2dbbdc", + "name": "Google Sheets - Upsert Banco Fulgencio", + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + } + }, + { + "parameters": { + "content": "# 🔄 SYNC TABLERO CDC → BANCO FULGENCIO\n\nSincroniza automáticamente un proyecto del Tablero CDC con el Banco de Propuestas de Fulgencio.\n\nFuncionamiento:\n\n1. Recibe mediante Webhook el identificador del proyecto.\n2. Normaliza y valida el project_id recibido.\n3. Consulta en Supabase los datos principales del proyecto.\n4. Recupera los enlaces asociados, como propuestas, presentaciones y artes finales.\n5. Convierte la información al formato requerido por el Banco Fulgencio.\n6. Valida si existen filas aptas para escribir.\n7. Inserta o actualiza el proyecto en Google Sheets.\n8. Devuelve al sistema solicitante el resultado de la sincronización.\n\nDatos procesados:\n\n- Identificador del proyecto.\n- Nombre o descripción de la propuesta.\n- Cliente y marca.\n- País o unidad de negocio.\n- Responsable del proyecto.\n- Estado y fechas disponibles.\n- Enlaces relacionados con la propuesta.\n- Información necesaria para las consultas de Fulgencio.\n\nReglas:\n\n- Supabase es la fuente oficial de los datos del proyecto.\n- Los enlaces se consultan por separado y se relacionan mediante el project_id.\n- El Upsert actualiza una fila existente o crea una nueva, evitando duplicados.\n- Solo se escribe cuando existe información válida para el Banco Fulgencio.\n- Si no hay filas procesables, el flujo termina correctamente sin modificar el Sheet.\n- Ambas ramas responden al Webhook para evitar solicitudes abiertas o tiempos de espera.", + "height": 960, + "width": 1792, + "color": "#24555C" + }, + "type": "n8n-nodes-base.stickyNote", + "typeVersion": 1, + "position": [ + -336, + -624 + ], + "id": "f561433a-a85c-497c-b3ef-fd36eef3b0b6", + "name": "Sticky Note" + } + ], + "pinData": {}, + "connections": { + "Webhook": { + "main": [ + [ + { + "node": "Code - Preparar project_id", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar project_id": { + "main": [ + [ + { + "node": "Supabase - Leer proyecto", + "type": "main", + "index": 0 + } + ] + ] + }, + "Supabase - Leer proyecto": { + "main": [ + [ + { + "node": "Supabase - Leer links del proyecto", + "type": "main", + "index": 0 + } + ] + ] + }, + "Supabase - Leer links del proyecto": { + "main": [ + [ + { + "node": "Code - Preparar filas Fulgencio", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar filas Fulgencio": { + "main": [ + [ + { + "node": "IF - Tiene filas para escribir?", + "type": "main", + "index": 0 + } + ] + ] + }, + "IF - Tiene filas para escribir?": { + "main": [ + [ + { + "node": "Google Sheets - Upsert Banco Fulgencio", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Respond to Webhook1", + "type": "main", + "index": 0 + } + ] + ] + }, + "Google Sheets - Upsert Banco Fulgencio": { + "main": [ + [ + { + "node": "Respond to Webhook", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": true, + "settings": { + "executionOrder": "v1", + "binaryMode": "separate", + "availableInMCP": true, + "timeSavedMode": "fixed", + "errorWorkflow": "puF4LUczoSz3hcek", + "timezone": "America/Santo_Domingo", + "callerPolicy": "workflowsFromSameOwner" + }, + "versionId": "5b0c04ba-7275-4352-b6af-323cd6ead268", + "meta": { + "templateCredsSetupCompleted": true, + "instanceId": "b4b77b17af092830e794eef639ce2f6d7daccf7eddc075060b03b3b6545aac70" + }, + "id": "k2NNakqj2TaP0HKy", + "tags": [] +} \ No newline at end of file diff --git a/Tablero CDC - Sync Proyecto a Sheet WEBHOOK.json b/Tablero CDC - Sync Proyecto a Sheet WEBHOOK.json new file mode 100644 index 0000000..aa3f64f --- /dev/null +++ b/Tablero CDC - Sync Proyecto a Sheet WEBHOOK.json @@ -0,0 +1,801 @@ +{ + "name": "Tablero CDC - Sync Proyecto a Sheet WEBHOOK", + "nodes": [ + { + "parameters": { + "url": "=https://dbit.digitalcompass.agency/rest/v1/tablero_cdc_projects?id=eq.{{ $('Code - Preparar project_id').first().json.project_id }}&select=*", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "apikey", + "value": "=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q" + }, + { + "name": "Authorization", + "value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q" + }, + { + "name": "Accept", + "value": "application/json" + }, + { + "name": "Content-Type", + "value": "application/json" + } + ] + }, + "options": {} + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.4, + "position": [ + 1824, + 288 + ], + "id": "5fdded5f-5a19-4f40-bc75-21ae92ac3df2", + "name": "Supabase - Leer proyecto" + }, + { + "parameters": { + "url": "=https://dbit.digitalcompass.agency/rest/v1/tablero_cdc_project_links?project_id=eq.{{ $('Code - Preparar project_id').first().json.project_id }}&select=*&order=created_at.asc", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "apikey", + "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q" + }, + { + "name": "Authorization", + "value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q" + }, + { + "name": "Accept", + "value": "application/json" + }, + { + "name": "Content-Type", + "value": "application/json" + } + ] + }, + "options": {} + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.4, + "position": [ + 2064, + 288 + ], + "id": "d2839f2d-af1f-49ab-bfd6-4c3b76f656ad", + "name": "Supabase - Leer links del proyecto", + "alwaysOutputData": true + }, + { + "parameters": { + "jsCode": "function getNodeJson(nodeName) {\n try {\n return $(nodeName).first().json;\n } catch (error) {\n return {};\n }\n}\n\nfunction getNodeItems(nodeName) {\n try {\n return $(nodeName).all();\n } catch (error) {\n return [];\n }\n}\n\nfunction normalizeProjectResponse(value) {\n if (Array.isArray(value)) return value[0] || null;\n return value || null;\n}\n\nfunction normalizeLinksResponse(items) {\n const links = [];\n\n for (const item of items) {\n const json = item.json;\n\n if (Array.isArray(json)) {\n links.push(...json);\n } else if (json && typeof json === 'object') {\n links.push(json);\n }\n }\n\n return links.filter(link => link && link.url);\n}\n\nfunction formatMonth(dateValue) {\n const date = dateValue ? new Date(dateValue) : new Date();\n\n if (Number.isNaN(date.getTime())) {\n return '';\n }\n\n const month = date.toLocaleDateString('es-DO', {\n month: 'long',\n });\n\n return month.charAt(0).toUpperCase() + month.slice(1);\n}\n\nfunction formatYear(dateValue) {\n const date = dateValue ? new Date(dateValue) : new Date();\n\n if (Number.isNaN(date.getTime())) {\n return String(new Date().getFullYear());\n }\n\n return String(date.getFullYear());\n}\n\nfunction formatMoney(value) {\n if (value === null || value === undefined || value === '') return '';\n return value;\n}\n\nfunction clean(value) {\n return String(value ?? '').trim();\n}\n\nfunction formatLinks(links, type) {\n const filtered = links.filter(link => String(link.link_type || '').toLowerCase() === type);\n\n if (!filtered.length) return '';\n\n return filtered\n .map(link => clean(link.url))\n .filter(Boolean)\n .join('\\n');\n}\n\nfunction findExistingSheetRow(projectId) {\n const sheetItems = getNodeItems('Sheets - Leer proyectos existentes');\n\n for (const item of sheetItems) {\n const row = item.json || {};\n const rowProjectId = clean(row['Project ID']);\n\n if (rowProjectId && rowProjectId === projectId) {\n return row;\n }\n }\n\n return null;\n}\n\nconst rawProject = getNodeJson('Supabase - Leer proyecto');\nconst project = normalizeProjectResponse(rawProject);\n\nif (!project || !project.id) {\n throw new Error('No se encontró el proyecto en Supabase. Revisa el project_id.');\n}\n\nconst linkItems = getNodeItems('Supabase - Leer links del proyecto');\nconst links = normalizeLinksResponse(linkItems);\n\nconst proposalLinks = formatLinks(links, 'proposal');\nconst finalArtLinks = formatLinks(links, 'final_art');\n\nconst createdAt = project.created_at || new Date().toISOString();\n\nconst existingSheetRow = findExistingSheetRow(project.id);\n\nconst sheetYear = existingSheetRow && clean(existingSheetRow['Año'])\n ? clean(existingSheetRow['Año'])\n : formatYear(createdAt);\n\nconst sheetMonth = existingSheetRow && clean(existingSheetRow['Mes'])\n ? clean(existingSheetRow['Mes'])\n : formatMonth(createdAt);\n\nreturn [\n {\n json: {\n 'Año': sheetYear,\n 'Mes': sheetMonth,\n 'Tipo o nombre de proyecto': project.title || '',\n 'Cliente': project.client || '',\n 'BU Solicita': project.country || '',\n 'Marca': project.brand || '',\n 'CM': project.country_manager || '',\n 'Status': project.status || '',\n 'Comentarios': project.requested_by ? `Solicitado por ${project.requested_by}` : '',\n 'Link de propuesta': proposalLinks,\n '$ Interno Cargado': formatMoney(project.internal_amount),\n 'Link del Brief': project.brief_link || '',\n 'Link de artes finales': finalArtLinks,\n 'Project ID': project.id,\n },\n },\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 2496, + 288 + ], + "id": "1a9e76bd-20f8-481b-82c1-95d631e261ae", + "name": "Code - Preparar fila Sheet" + }, + { + "parameters": { + "operation": "appendOrUpdate", + "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": 1563472127, + "mode": "list", + "cachedResultName": "PROYECTOS 2026", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1MfoIs_y7-4ad-g5dxzOo8oAFIdraA_ZT4k-OCGMtT4Q/edit#gid=1563472127" + }, + "columns": { + "mappingMode": "defineBelow", + "value": { + "Año": "={{ $json[\"Año\"] }}", + "Mes": "={{ $json[\"Mes\"] }}", + "Tipo o nombre de proyecto": "={{ $json[\"Tipo o nombre de proyecto\"] }}", + "Cliente": "={{ $json[\"Cliente\"] }}", + "BU Solicita": "={{ $json[\"BU Solicita\"] }}", + "Marca": "={{ $json[\"Marca\"] }}", + "CM": "={{ $json[\"CM\"] }}", + "Status": "={{ $json[\"Status\"] }}", + "Comentarios": "={{ $json[\"Comentarios\"] }}", + "Link de propuesta": "={{ $json[\"Link de propuesta\"] }}", + "$ Interno Cargado": "={{ $json[\"$ Interno Cargado\"] }}", + "Link del Brief": "={{ $json[\"Link del Brief\"] }}", + "Link de artes finales": "={{ $json[\"Link de artes finales\"] }}", + "Project ID": "={{ $json[\"Project ID\"] }}" + }, + "matchingColumns": [ + "Project ID" + ], + "schema": [ + { + "id": "Año", + "displayName": "Año", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "Mes", + "displayName": "Mes", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "Tipo o nombre de proyecto", + "displayName": "Tipo o nombre de proyecto", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "Cliente", + "displayName": "Cliente", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "BU Solicita", + "displayName": "BU Solicita", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "Marca", + "displayName": "Marca", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "CM", + "displayName": "CM", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "Status", + "displayName": "Status", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "Comentarios", + "displayName": "Comentarios", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "Link de propuesta", + "displayName": "Link de propuesta", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "$ Interno Cargado", + "displayName": "$ Interno Cargado", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "Link del Brief", + "displayName": "Link del Brief", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "Link de artes finales", + "displayName": "Link de artes finales", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "Project ID", + "displayName": "Project ID", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + } + ], + "attemptToConvertTypes": false, + "convertFieldsToString": false + }, + "options": { + "locationDefine": { + "values": { + "headerRow": 3, + "firstDataRow": 4 + } + } + } + }, + "type": "n8n-nodes-base.googleSheets", + "typeVersion": 4.7, + "position": [ + 2736, + 288 + ], + "id": "d66bf9b4-9694-4f43-8dd6-4e9efe4eece8", + "name": "Sheets - Upsert proyecto", + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + } + }, + { + "parameters": { + "httpMethod": "POST", + "path": "tablero-cdc-sync-proyecto", + "responseMode": "responseNode", + "options": {} + }, + "type": "n8n-nodes-base.webhook", + "typeVersion": 2.1, + "position": [ + 208, + 272 + ], + "id": "b5d0c07b-8b2d-437c-9475-530f64e84247", + "name": "Webhook", + "webhookId": "4f237f0a-7607-4a8b-9020-11bc75a6ae32" + }, + { + "parameters": { + "jsCode": "const input = $json || {};\nconst body = input.body || input;\n\nconst projectId =\n body.project_id ||\n body.projectId ||\n body.id ||\n '';\n\nconst action =\n String(body.action || 'sync').trim().toLowerCase();\n\nif (!projectId) {\n throw new Error('Falta project_id en el payload del webhook');\n}\n\nreturn [\n {\n json: {\n action,\n project_id: projectId,\n received_at: new Date().toISOString(),\n },\n },\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 416, + 272 + ], + "id": "017aafcb-5f7a-407d-8224-caf365186b78", + "name": "Code - Preparar project_id" + }, + { + "parameters": { + "respondWith": "json", + "responseBody": "={{\n {\n ok: true,\n message: 'Proyecto sincronizado con Google Sheet',\n project_id: $('Code - Preparar project_id').first().json.project_id\n }\n}}", + "options": { + "responseCode": 200 + } + }, + "type": "n8n-nodes-base.respondToWebhook", + "typeVersion": 1.5, + "position": [ + 3216, + 224 + ], + "id": "425a3374-03dc-4424-b089-3d86efbd565e", + "name": "Respond to Webhook" + }, + { + "parameters": { + "content": "## Sync Proyecto a Sheet WEBHOOK\n\nFlujo de producción que recibe project_id desde la app Tablero CDC y sincroniza el proyecto con Google Sheet PROYECTOS 2026.\n\nFlujo:\nWebhook -> preparar project_id -> leer projects -> leer project_links -> preparar fila -> upsert en Sheet -> responder webhook.\n\nNotas:\n- La app llama este flujo usando VITE_WEBHOOK_URL.\n- El upsert usa Project ID.\n- Encabezados del Sheet: fila 3.\n- Datos desde fila 4.\n- Filas viejas sin Project ID quedan como histórico.\n- Leer links del proyecto debe tener Always Output Data activo para que proyectos sin links también se peguen.\n\nSi cambia Supabase empresarial/self-hosted, actualizar URL y keys en los HTTP Request.", + "height": 704, + "width": 3380, + "color": 5 + }, + "type": "n8n-nodes-base.stickyNote", + "typeVersion": 1, + "position": [ + 160, + -208 + ], + "id": "16a4a31a-a288-45f5-86fd-be4d92a7a2c1", + "name": "Sticky Note" + }, + { + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "36231932-b806-4b8a-8d59-e9a74ef4c74b", + "leftValue": "={{ $json.action }}", + "rightValue": "delete", + "operator": { + "type": "string", + "operation": "equals", + "name": "filter.operator.equals" + } + } + ], + "combinator": "and" + }, + "options": {} + }, + "type": "n8n-nodes-base.if", + "typeVersion": 2.3, + "position": [ + 688, + 272 + ], + "id": "e891ba4b-23d2-4432-bb33-deffbee8097a", + "name": "IF - Acción eliminar?" + }, + { + "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": 1563472127, + "mode": "list", + "cachedResultName": "PROYECTOS 2026", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1MfoIs_y7-4ad-g5dxzOo8oAFIdraA_ZT4k-OCGMtT4Q/edit#gid=1563472127" + }, + "options": { + "dataLocationOnSheet": { + "values": { + "rangeDefinition": "specifyRange", + "headerRow": 3, + "firstDataRow": 4 + } + } + } + }, + "type": "n8n-nodes-base.googleSheets", + "typeVersion": 4.7, + "position": [ + 1200, + -96 + ], + "id": "df240622-6ec0-44cb-b945-28c09bd50496", + "name": "Sheets - Leer proyectos para eliminar", + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + } + }, + { + "parameters": { + "jsCode": "const projectId = $('Code - Preparar project_id').first().json.project_id;\n\nconst rows = $input.all();\n\n// En este Sheet los encabezados están en fila 3 y los datos empiezan en fila 4.\n// Si Google Sheets no devuelve row_number, lo calculamos con index + 4.\nconst FIRST_DATA_ROW = 4;\n\nconst matches = [];\n\nrows.forEach((item, index) => {\n const row = item.json;\n\n const rowProjectId =\n row['Project ID'] ||\n row['project_id'] ||\n row['PROJECT ID'] ||\n '';\n\n if (String(rowProjectId).trim() === String(projectId).trim()) {\n const rowNumber =\n row.row_number ||\n row.rowNumber ||\n row.__rowNumber ||\n row.rowIndex ||\n index + FIRST_DATA_ROW;\n\n matches.push({\n row,\n row_number: Number(rowNumber),\n });\n }\n});\n\nif (matches.length === 0) {\n return [\n {\n json: {\n ok: true,\n action: 'delete',\n project_id: projectId,\n found: false,\n message: 'No se encontró fila en Google Sheet para ese Project ID. Se considera eliminado.',\n },\n },\n ];\n}\n\nreturn matches.map((match) => ({\n json: {\n ok: true,\n action: 'delete',\n project_id: projectId,\n found: true,\n row_number: match.row_number,\n row: match.row,\n },\n}));" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1408, + -96 + ], + "id": "ec6056cf-99b9-4b23-9ee4-62b71100aaa2", + "name": "Code - Buscar fila a eliminar" + }, + { + "parameters": { + "operation": "delete", + "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": 1563472127, + "mode": "list", + "cachedResultName": "PROYECTOS 2026", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1MfoIs_y7-4ad-g5dxzOo8oAFIdraA_ZT4k-OCGMtT4Q/edit#gid=1563472127" + }, + "startIndex": "={{ $json.row_number }}" + }, + "type": "n8n-nodes-base.googleSheets", + "typeVersion": 4.7, + "position": [ + 1840, + -112 + ], + "id": "fc0117c5-9b01-44cc-b8d4-659ed9784fc1", + "name": "Sheets - Eliminar fila proyecto", + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + } + }, + { + "parameters": { + "respondWith": "json", + "responseBody": "{\n \"ok\": true,\n \"action\": \"delete\",\n \"message\": \"Proyecto eliminado del Google Sheet\"\n}", + "options": {} + }, + "type": "n8n-nodes-base.respondToWebhook", + "typeVersion": 1.5, + "position": [ + 2048, + -112 + ], + "id": "02857828-6a02-40a6-ade7-203361f0698b", + "name": "Respond - Proyecto eliminado del Sheet" + }, + { + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "b513a96e-250f-4730-a926-bcc438daa0c6", + "leftValue": "={{ $json.found }}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "options": {} + }, + "type": "n8n-nodes-base.if", + "typeVersion": 2.3, + "position": [ + 1616, + -96 + ], + "id": "c281d09b-16da-450b-bcf8-0412f6658dcb", + "name": "IF - Fila encontrada?" + }, + { + "parameters": { + "respondWith": "json", + "responseBody": "{\n \"ok\": true,\n \"action\": \"delete\",\n \"found\": false,\n \"message\": \"No se encontró fila en Google Sheet para ese Project ID. Se considera eliminado.\"\n}", + "options": { + "responseCode": 200 + } + }, + "type": "n8n-nodes-base.respondToWebhook", + "typeVersion": 1.5, + "position": [ + 1856, + 112 + ], + "id": "182432ed-700f-4fff-84b9-8aa45fd7c0b9", + "name": "Respond - Proyecto no encontrado en Sheet" + }, + { + "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": 1563472127, + "mode": "list", + "cachedResultName": "PROYECTOS 2026", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1MfoIs_y7-4ad-g5dxzOo8oAFIdraA_ZT4k-OCGMtT4Q/edit#gid=1563472127" + }, + "options": { + "dataLocationOnSheet": { + "values": { + "rangeDefinition": "specifyRangeA1", + "range": "A3:N" + } + } + } + }, + "type": "n8n-nodes-base.googleSheets", + "typeVersion": 4.7, + "position": [ + 2272, + 288 + ], + "id": "b4f4366e-3381-4692-8740-8f7287eeb9a4", + "name": "Sheets - Leer proyectos existentes", + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + } + }, + { + "parameters": { + "method": "POST", + "url": "https://agenteit.digitalcompass.agency/webhook/tablero-cdc-sync-fulgencio-ia", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + {} + ] + }, + "sendBody": true, + "bodyParameters": { + "parameters": [ + { + "name": "action", + "value": "sync" + }, + { + "name": "project_id", + "value": "={{ $('Code - Preparar project_id').first().json.project_id }}" + } + ] + }, + "options": {} + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.4, + "position": [ + 2944, + 288 + ], + "id": "f2b3649c-0311-497e-bb48-cc793a55e51d", + "name": "HTTP - Sync Banco Fulgencio", + "retryOnFail": true, + "waitBetweenTries": 1000, + "onError": "continueRegularOutput" + } + ], + "pinData": {}, + "connections": { + "Supabase - Leer proyecto": { + "main": [ + [ + { + "node": "Supabase - Leer links del proyecto", + "type": "main", + "index": 0 + } + ] + ] + }, + "Supabase - Leer links del proyecto": { + "main": [ + [ + { + "node": "Sheets - Leer proyectos existentes", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar fila Sheet": { + "main": [ + [ + { + "node": "Sheets - Upsert proyecto", + "type": "main", + "index": 0 + } + ] + ] + }, + "Webhook": { + "main": [ + [ + { + "node": "Code - Preparar project_id", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar project_id": { + "main": [ + [ + { + "node": "IF - Acción eliminar?", + "type": "main", + "index": 0 + } + ] + ] + }, + "Sheets - Upsert proyecto": { + "main": [ + [ + { + "node": "HTTP - Sync Banco Fulgencio", + "type": "main", + "index": 0 + } + ] + ] + }, + "IF - Acción eliminar?": { + "main": [ + [ + { + "node": "Sheets - Leer proyectos para eliminar", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Supabase - Leer proyecto", + "type": "main", + "index": 0 + } + ] + ] + }, + "Sheets - Leer proyectos para eliminar": { + "main": [ + [ + { + "node": "Code - Buscar fila a eliminar", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Buscar fila a eliminar": { + "main": [ + [ + { + "node": "IF - Fila encontrada?", + "type": "main", + "index": 0 + } + ] + ] + }, + "Sheets - Eliminar fila proyecto": { + "main": [ + [ + { + "node": "Respond - Proyecto eliminado del Sheet", + "type": "main", + "index": 0 + } + ] + ] + }, + "IF - Fila encontrada?": { + "main": [ + [ + { + "node": "Sheets - Eliminar fila proyecto", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Respond - Proyecto no encontrado en Sheet", + "type": "main", + "index": 0 + } + ] + ] + }, + "Sheets - Leer proyectos existentes": { + "main": [ + [ + { + "node": "Code - Preparar fila Sheet", + "type": "main", + "index": 0 + } + ] + ] + }, + "HTTP - Sync Banco Fulgencio": { + "main": [ + [ + { + "node": "Respond to Webhook", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": true, + "settings": { + "executionOrder": "v1", + "binaryMode": "separate", + "availableInMCP": true, + "timeSavedMode": "fixed", + "errorWorkflow": "puF4LUczoSz3hcek", + "callerPolicy": "workflowsFromSameOwner", + "timezone": "America/Santo_Domingo" + }, + "versionId": "cc3433da-498f-47e0-902a-bf494199dcf5", + "meta": { + "templateCredsSetupCompleted": true, + "instanceId": "b4b77b17af092830e794eef639ce2f6d7daccf7eddc075060b03b3b6545aac70" + }, + "id": "d39DtIUTQzduqX0v", + "tags": [] +} \ No newline at end of file diff --git a/Tablero CDC - Sync Tarifario Sheet a Supabase.json b/Tablero CDC - Sync Tarifario Sheet a Supabase.json new file mode 100644 index 0000000..fa8edd4 --- /dev/null +++ b/Tablero CDC - Sync Tarifario Sheet a Supabase.json @@ -0,0 +1,449 @@ +{ + "name": "Tablero CDC - Sync Tarifario Sheet a Supabase", + "nodes": [ + { + "parameters": { + "jsCode": "function cleanText(value) {\n return String(value ?? \"\").trim();\n}\n\nfunction normalizeForCompare(value) {\n return cleanText(value)\n .toLowerCase()\n .normalize(\"NFD\")\n .replace(/[\\u0300-\\u036f]/g, \"\");\n}\n\nfunction slugify(value) {\n return String(value ?? \"\")\n .trim()\n .toLowerCase()\n .normalize(\"NFD\")\n .replace(/[\\u0300-\\u036f]/g, \"\")\n .replace(/&/g, \"y\")\n .replace(/ñ/g, \"n\")\n .replace(/[^a-z0-9]+/g, \"_\")\n .replace(/^_+|_+$/g, \"\");\n}\n\nfunction parseAmountRange(value) {\n const raw = cleanText(value);\n\n if (!raw) {\n return {\n reference: \"\",\n reference_min: null,\n reference_max: null,\n };\n }\n\n const reference = raw.replace(/\\s+/g, \" \");\n\n const withoutCurrency = reference\n .replace(/USD/gi, \"\")\n .replace(/US\\$/gi, \"\")\n .replace(/\\$/g, \"\")\n .replace(/\\+/g, \"\")\n .trim();\n\n const matches = withoutCurrency.match(/-?\\d[\\d,]*(\\.\\d+)?/g) || [];\n\n const numbers = matches\n .map((match) => Number(match.replace(/,/g, \"\")))\n .filter((number) => Number.isFinite(number));\n\n if (numbers.length === 0) {\n return {\n reference,\n reference_min: null,\n reference_max: null,\n };\n }\n\n if (numbers.length === 1) {\n return {\n reference,\n reference_min: numbers[0],\n reference_max: numbers[0],\n };\n }\n\n const min = Math.min(numbers[0], numbers[1]);\n const max = Math.max(numbers[0], numbers[1]);\n\n return {\n reference,\n reference_min: min,\n reference_max: max,\n };\n}\n\nfunction normalizeSection(value) {\n const raw = normalizeForCompare(value);\n\n if (!raw) {\n throw new Error(\"La columna Sección está vacía en una fila del tarifario.\");\n }\n\n if (raw === \"tarifario grafico cdc\") {\n return \"grafico\";\n }\n\n if (raw === \"estrategia y creatividad\") {\n return \"estrategia\";\n }\n\n throw new Error(\n `Sección inválida en tarifario: \"${value}\". Usa solo \"Tarifario Gráfico CDC\" o \"Estrategia y Creatividad\".`\n );\n}\n\nfunction buildBaseId(row, section, category, service, workTypeId) {\n const rawCodigoBase = cleanText(row[\"Código base\"]);\n\n if (rawCodigoBase) {\n return slugify(rawCodigoBase);\n }\n\n return slugify(\n [section, category, service, workTypeId].filter(Boolean).join(\"_\")\n );\n}\n\nfunction getSourceRowNumber(row, index) {\n return (\n row.row_number ||\n row.rowNumber ||\n row.__rowNumber ||\n index + 2\n );\n}\n\nconst records = [];\nconst generatedCodeUpdates = [];\n\nitems.forEach((item, index) => {\n const row = item.json;\n\n const rawSection = cleanText(row[\"Sección\"]);\n const rawCategory = cleanText(row[\"Categoría\"]);\n const rawService = cleanText(row[\"Servicio / ítem\"]);\n const rawWorkType = cleanText(row[\"Tipo de trabajo\"] || \"Referencia\");\n const shortLabel = cleanText(row[\"Etiqueta corta\"] || \"Ref.\");\n const hourReference = cleanText(row[\"Hora hombre ref.\"]);\n const notes = cleanText(row[\"Observaciones / incluye\"]);\n\n // Ignora filas totalmente vacías.\n const hasAnyBusinessData = [\n rawSection,\n rawCategory,\n rawService,\n rawWorkType,\n row[\"Nivel 1 nombre\"],\n row[\"Nivel 1 monto/rango\"],\n row[\"Nivel 2 nombre\"],\n row[\"Nivel 2 monto/rango\"],\n row[\"Nivel 3 nombre\"],\n row[\"Nivel 3 monto/rango\"],\n ].some((value) => cleanText(value));\n\n if (!hasAnyBusinessData) {\n return;\n }\n\n const section = normalizeSection(rawSection);\n const category = rawCategory;\n const service = rawService;\n const workTypeLabel = rawWorkType;\n const workTypeId = slugify(workTypeLabel || \"reference\") || \"reference\";\n\n if (!category || !service) {\n throw new Error(\n `Fila ${getSourceRowNumber(row, index)} incompleta: Categoría y Servicio / ítem son obligatorios.`\n );\n }\n\n const catalogItemId = slugify(\n [section, category, service].filter(Boolean).join(\"_\")\n );\n\n const baseId = buildBaseId(row, section, category, service, workTypeId);\n\n if (!baseId) {\n throw new Error(`No se pudo generar Código base en la fila ${getSourceRowNumber(row, index)}.`);\n }\n\n const sourceRowNumber = getSourceRowNumber(row, index);\n\n if (!cleanText(row[\"Código base\"])) {\n generatedCodeUpdates.push({\n row_number: sourceRowNumber,\n codigo_base: baseId,\n });\n }\n\n const levels = [\n {\n number: 1,\n name: cleanText(row[\"Nivel 1 nombre\"]),\n amountRange: cleanText(row[\"Nivel 1 monto/rango\"]),\n note: cleanText(row[\"Nivel 1 nota\"]),\n },\n {\n number: 2,\n name: cleanText(row[\"Nivel 2 nombre\"]),\n amountRange: cleanText(row[\"Nivel 2 monto/rango\"]),\n note: cleanText(row[\"Nivel 2 nota\"]),\n },\n {\n number: 3,\n name: cleanText(row[\"Nivel 3 nombre\"]),\n amountRange: cleanText(row[\"Nivel 3 monto/rango\"]),\n note: cleanText(row[\"Nivel 3 nota\"]),\n },\n ];\n\n const validLevels = levels.filter((level) => level.name && level.amountRange);\n\n if (validLevels.length === 0) {\n throw new Error(\n `Fila ${sourceRowNumber}: agrega al menos un nivel con nombre y monto/rango.`\n );\n }\n\n for (const level of validLevels) {\n const levelLabel = level.name;\n const levelId = slugify(levelLabel || \"project\") || \"project\";\n const amount = parseAmountRange(level.amountRange);\n\n const id = slugify(\n [baseId, levelId].filter(Boolean).join(\"_\")\n );\n\n records.push({\n id,\n catalog_item_id: catalogItemId,\n section,\n category,\n service,\n notes,\n work_type_id: workTypeId,\n work_type_label: workTypeLabel,\n work_type_short_label: shortLabel,\n hour_reference: hourReference,\n level_id: levelId,\n level_label: levelLabel,\n reference: amount.reference,\n reference_min: amount.reference_min,\n reference_max: amount.reference_max,\n level_hint: level.note,\n sort_order: 10000 + index * 10 + level.number,\n is_active: true,\n });\n }\n});\n\nif (records.length === 0) {\n throw new Error(\"No se encontraron tarifas válidas. Se detiene para no afectar el catálogo.\");\n}\n\nconst seen = new Set();\n\nfor (const record of records) {\n if (seen.has(record.id)) {\n throw new Error(`Código único duplicado detectado en tarifario: ${record.id}`);\n }\n\n seen.add(record.id);\n\n if (\n record.reference_min !== null &&\n record.reference_max !== null &&\n record.reference_min > record.reference_max\n ) {\n throw new Error(`La tarifa ${record.id} tiene mínimo mayor que máximo.`);\n }\n}\n\nreturn [\n {\n json: {\n total_source_rows: items.length,\n total_records_to_sync: records.length,\n total_generated_codes: generatedCodeUpdates.length,\n records,\n generated_code_updates: generatedCodeUpdates,\n },\n },\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 416, + 0 + ], + "id": "25030323-2bde-460c-b1ad-02e6260d67da", + "name": "Code - Normalizar Tarifario" + }, + { + "parameters": { + "method": "POST", + "url": "https://dbit.digitalcompass.agency/rest/v1/tablero_cdc_tariff_catalog?on_conflict=id", + "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": "a21e1648-fd8c-4cf3-acc3-50f91af1e4d9", + "name": "Supabase - Upsert Tarifario" + }, + { + "parameters": { + "documentId": { + "__rl": true, + "value": "1BGTCXbeqo-QAaoBx2pHJo9p7Trzd7Qh0FpEFypSyTAg", + "mode": "list", + "cachedResultName": "Tarifario de Creatividad - Sep 2004", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1BGTCXbeqo-QAaoBx2pHJo9p7Trzd7Qh0FpEFypSyTAg/edit?usp=drivesdk" + }, + "sheetName": { + "__rl": true, + "value": 1041164995, + "mode": "list", + "cachedResultName": "Catálogo Tarifario App", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1BGTCXbeqo-QAaoBx2pHJo9p7Trzd7Qh0FpEFypSyTAg/edit#gid=1041164995" + }, + "options": { + "dataLocationOnSheet": { + "values": { + "rangeDefinition": "detectAutomatically" + } + } + } + }, + "type": "n8n-nodes-base.googleSheets", + "typeVersion": 4.7, + "position": [ + 208, + 0 + ], + "id": "e6d4f9ef-30da-4640-9002-96b43aa48729", + "name": "Google Sheets - Leer Catálogo Tarifario App", + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + } + }, + { + "parameters": { + "jsCode": "const source = $('Code - Normalizar Tarifario').first().json;\n\nconst updates = source.generated_code_updates || [];\n\nreturn updates.map((update) => ({\n json: {\n row_number: update.row_number,\n codigo_base: update.codigo_base,\n },\n}));" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 832, + 0 + ], + "id": "247f2f76-a1e4-4e51-b35f-a32bdaadf354", + "name": "Code - Preparar códigos generados" + }, + { + "parameters": { + "operation": "update", + "documentId": { + "__rl": true, + "value": "1BGTCXbeqo-QAaoBx2pHJo9p7Trzd7Qh0FpEFypSyTAg", + "mode": "list", + "cachedResultName": "Tarifario de Creatividad - Sep 2004", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1BGTCXbeqo-QAaoBx2pHJo9p7Trzd7Qh0FpEFypSyTAg/edit?usp=drivesdk" + }, + "sheetName": { + "__rl": true, + "value": 1041164995, + "mode": "list", + "cachedResultName": "Catálogo Tarifario App", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1BGTCXbeqo-QAaoBx2pHJo9p7Trzd7Qh0FpEFypSyTAg/edit#gid=1041164995" + }, + "columns": { + "mappingMode": "defineBelow", + "value": { + "row_number": "={{ $json[\"row_number\"] }}", + "Código base": "={{ $json[\"codigo_base\"] }}" + }, + "matchingColumns": [ + "row_number" + ], + "schema": [ + { + "id": "Código base", + "displayName": "Código base", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "Sección", + "displayName": "Sección", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "Categoría", + "displayName": "Categoría", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "Servicio / ítem", + "displayName": "Servicio / ítem", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "Tipo de trabajo", + "displayName": "Tipo de trabajo", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "Etiqueta corta", + "displayName": "Etiqueta corta", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "Hora hombre ref.", + "displayName": "Hora hombre ref.", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "Nivel 1 nombre", + "displayName": "Nivel 1 nombre", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "Nivel 1 monto/rango", + "displayName": "Nivel 1 monto/rango", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "Nivel 1 nota", + "displayName": "Nivel 1 nota", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "Nivel 2 nombre", + "displayName": "Nivel 2 nombre", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "Nivel 2 monto/rango", + "displayName": "Nivel 2 monto/rango", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "Nivel 2 nota", + "displayName": "Nivel 2 nota", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "Nivel 3 nombre", + "displayName": "Nivel 3 nombre", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "Nivel 3 monto/rango", + "displayName": "Nivel 3 monto/rango", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "Nivel 3 nota", + "displayName": "Nivel 3 nota", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "Observaciones / incluye", + "displayName": "Observaciones / incluye", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "row_number", + "displayName": "row_number", + "required": false, + "defaultMatch": false, + "display": true, + "type": "number", + "canBeUsedToMatch": true, + "readOnly": true, + "removed": false + } + ], + "attemptToConvertTypes": false, + "convertFieldsToString": false + }, + "options": {} + }, + "type": "n8n-nodes-base.googleSheets", + "typeVersion": 4.7, + "position": [ + 1072, + 0 + ], + "id": "60367c04-79f6-4c8a-99d1-bc9369c8ca61", + "name": "Google Sheets - Escribir códigos únicos faltantes", + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + } + }, + { + "parameters": { + "rule": { + "interval": [ + { + "field": "hours", + "hoursInterval": 8 + } + ] + } + }, + "type": "n8n-nodes-base.scheduleTrigger", + "typeVersion": 1.3, + "position": [ + -16, + 0 + ], + "id": "1a45e645-65af-4727-bf4f-15c7000ba2a2", + "name": "Schedule Trigger" + }, + { + "parameters": { + "content": "## 💵 SYNC TARIFARIO CDC → SUPABASE\n\nEste flujo sincroniza automáticamente el tarifario editable desde Google Sheets hacia Supabase para que la app Tablero CDC use el catálogo actualizado.\n\nFrecuencia:\n- Cada 8 horas.\n\nProceso:\n1. Lee el Sheet “Catálogo Tarifario App”.\n2. Normaliza sección, categoría, servicio, tipo de trabajo y niveles.\n3. Convierte “monto/rango” en reference, reference_min y reference_max.\n4. Genera códigos únicos cuando faltan.\n5. Hace upsert en Supabase: tablero_cdc_tariff_catalog.\n6. Escribe de vuelta en el Sheet los códigos base generados.\n\nReglas importantes:\n- No sincroniza filas vacías.\n- Cada fila debe tener categoría, servicio y al menos un nivel con monto/rango.\n- Si hay códigos duplicados o secciones inválidas, el flujo se detiene para proteger el catálogo.\n- El Sheet es la fuente humana editable; Supabase es la fuente usada por la app.", + "height": 640, + "width": 1408, + "color": 5 + }, + "type": "n8n-nodes-base.stickyNote", + "typeVersion": 1, + "position": [ + -64, + -432 + ], + "id": "23c63e0a-2e95-4ec5-b075-d08ae68754fc", + "name": "Sticky Note" + } + ], + "pinData": {}, + "connections": { + "Code - Normalizar Tarifario": { + "main": [ + [ + { + "node": "Supabase - Upsert Tarifario", + "type": "main", + "index": 0 + } + ] + ] + }, + "Supabase - Upsert Tarifario": { + "main": [ + [ + { + "node": "Code - Preparar códigos generados", + "type": "main", + "index": 0 + } + ] + ] + }, + "Google Sheets - Leer Catálogo Tarifario App": { + "main": [ + [ + { + "node": "Code - Normalizar Tarifario", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar códigos generados": { + "main": [ + [ + { + "node": "Google Sheets - Escribir códigos únicos faltantes", + "type": "main", + "index": 0 + } + ] + ] + }, + "Schedule Trigger": { + "main": [ + [ + { + "node": "Google Sheets - Leer Catálogo Tarifario App", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": true, + "settings": { + "executionOrder": "v1", + "binaryMode": "separate", + "availableInMCP": true, + "timeSavedMode": "fixed", + "errorWorkflow": "puF4LUczoSz3hcek", + "timezone": "America/Santo_Domingo", + "callerPolicy": "workflowsFromSameOwner" + }, + "versionId": "4ea3c2ee-0518-4a55-8c9a-ccffb26f471a", + "meta": { + "templateCredsSetupCompleted": true, + "instanceId": "b4b77b17af092830e794eef639ce2f6d7daccf7eddc075060b03b3b6545aac70" + }, + "id": "7eyfZEkGuDSUodDX", + "tags": [] +} \ No newline at end of file