Last active
November 10, 2025 17:01
-
-
Save mateusrovedaa/9a81c2ea328011684568aae89771c5d5 to your computer and use it in GitHub Desktop.
bot-telegram-n8n-finances
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| { | |
| "name": "Bot ROVEEb", | |
| "nodes": [ | |
| { | |
| "parameters": { | |
| "rules": { | |
| "values": [ | |
| { | |
| "conditions": { | |
| "options": { | |
| "caseSensitive": true, | |
| "leftValue": "", | |
| "typeValidation": "strict", | |
| "version": 2 | |
| }, | |
| "conditions": [ | |
| { | |
| "leftValue": "={{ $json.message.text }}", | |
| "rightValue": "", | |
| "operator": { | |
| "type": "string", | |
| "operation": "exists", | |
| "singleValue": true | |
| }, | |
| "id": "0f04348c-a5af-4874-a8cd-8da747c5271f" | |
| } | |
| ], | |
| "combinator": "and" | |
| }, | |
| "renameOutput": true, | |
| "outputKey": "text" | |
| }, | |
| { | |
| "conditions": { | |
| "options": { | |
| "caseSensitive": true, | |
| "leftValue": "", | |
| "typeValidation": "strict", | |
| "version": 2 | |
| }, | |
| "conditions": [ | |
| { | |
| "id": "a122a7ee-7f24-4d3b-aaf5-267688ed7176", | |
| "leftValue": "={{ $json.message.voice.file_id }}", | |
| "rightValue": "", | |
| "operator": { | |
| "type": "string", | |
| "operation": "exists", | |
| "singleValue": true | |
| } | |
| } | |
| ], | |
| "combinator": "and" | |
| }, | |
| "renameOutput": true, | |
| "outputKey": "voice" | |
| }, | |
| { | |
| "conditions": { | |
| "options": { | |
| "caseSensitive": true, | |
| "leftValue": "", | |
| "typeValidation": "strict", | |
| "version": 2 | |
| }, | |
| "conditions": [ | |
| { | |
| "id": "507ebe51-f956-4fdd-bd2b-6589a7e3c206", | |
| "leftValue": "={{ $json.message.document.file_id }}", | |
| "rightValue": "", | |
| "operator": { | |
| "type": "string", | |
| "operation": "exists", | |
| "singleValue": true | |
| } | |
| } | |
| ], | |
| "combinator": "and" | |
| }, | |
| "renameOutput": true, | |
| "outputKey": "csv" | |
| } | |
| ] | |
| }, | |
| "options": {} | |
| }, | |
| "type": "n8n-nodes-base.switch", | |
| "typeVersion": 3.2, | |
| "position": [ | |
| -2320, | |
| -672 | |
| ], | |
| "id": "571cb7f5-a835-4bb7-8ea9-61792de2f361", | |
| "name": "Switch" | |
| }, | |
| { | |
| "parameters": { | |
| "resource": "audio", | |
| "operation": "transcribe", | |
| "options": { | |
| "language": "pt" | |
| } | |
| }, | |
| "type": "@n8n/n8n-nodes-langchain.openAi", | |
| "typeVersion": 1.8, | |
| "position": [ | |
| -1888, | |
| -656 | |
| ], | |
| "id": "1dbe237c-c086-4c74-a292-1cab20d9e640", | |
| "name": "OpenAI", | |
| "retryOnFail": true, | |
| "credentials": { | |
| "openAiApi": { | |
| "id": "ASEhNATa5Q5yMLGg", | |
| "name": "OpenAi account" | |
| } | |
| } | |
| }, | |
| { | |
| "parameters": { | |
| "modelId": { | |
| "__rl": true, | |
| "value": "gpt-5-nano", | |
| "mode": "list", | |
| "cachedResultName": "GPT-5-NANO" | |
| }, | |
| "responses": { | |
| "values": [ | |
| { | |
| "content": "=Você é um extrator de transações financeiras em PT-BR no n8n. Sua tarefa é LER o texto bruto e DEVOLVER apenas JSON válido seguindo o esquema abaixo.\n\nEntrada (texto bruto):\n{{$json[\"message\"]}}\n\nSaída (obrigatória, SOMENTE JSON válido):\n{\"transactions\":[ ... ]} \nSe nada for encontrado: {\"transactions\":[]}\n\nPara cada item em \"transactions\" use exatamente estas chaves:\n- categoria: UMA dentre {Alimentação,Transporte,Moradia,Lazer,Saúde,Compras,Serviços,Educação,Dívidas/Empréstimos,Cartão de Crédito,Viagem,Outros}. Se for ENTRADA, use \"Receita\".\n- descricao: resumo curto, normalizado e legível (ex.: \"Almoço no X\", \"Gasolina no Y\"). NÃO escreva “compra de”, “recebido de”, “categoria…”.\n- valor: número BRL com ponto decimal (ex.: 50.00). Converta valores por extenso (ex.: \"cinquenta reais\" → 50.00).\n- data: AAAA-MM-DD. Inferir “hoje/ontem/dia da semana” considerando hoje = {{$now.setZone('America/Sao_Paulo').toFormat('yyyy-MM-dd HH:mm:ss')}}. Se ausente, usar a data de hoje.\n- tipo: \"entrada\" (receita/salário/recebido) ou \"saida\" (despesa/compra/custo).\n\nREGRAS DE NORMALIZAÇÃO (aplique antes de classificar):\n1) Remova caracteres de ruído em nomes/descrições: asterisco (*) , barras (/), sublinhado (_), hífen isolado (- quando usado como separador), pontos isolados (.) e múltiplos espaços → substitua por um único espaço.\n2) Aparar espaços no início/fim.\n3) \"Formato de frase\": \n - Coloque iniciais de palavras em Maiúsculas e demais minúsculas (Title Case), mantendo minúsculas para conectivos/curtas: {de, da, do, das, dos, e, em, no, na, nos, nas, com, para, por}.\n - Preserve siglas comuns (2–4 letras) se vierem todas em maiúsculas (ex.: \"USP\", \"DM”, \"IFD\" permanecem maiúsculas).\n4) Exemplos:\n - \"PET LOVE*CLUBE\" → \"Pet Love Clube\"\n - \"UBER * PENDING\" → \"Uber Pending\"\n - \"FARMACIA SAO JOAO\" → \"Farmacia São João\" (não invente acentos se não souber)\n\nREGRAS DE VALOR:\n- Aceite formatos: \"R$ 3.032,81\", \"R$ -50,00\", \"50,00\", \"-12,06\", ou por extenso (\"cinquenta reais\").\n- Converter para número com ponto decimal: 3032.81, -50.00, etc.\n- Se o valor for negativo, respeite o sinal no número final.\n- Se houver mais de um valor no texto, escolha o da transação em questão.\n\nREGRAS DE DATA:\n- Se houver \"hoje/ontem/anteontem\" ou \"segunda/terça/...\": infira a data relativa usando a referência de \"hoje\" acima (America/Sao_Paulo).\n- Se a data estiver ausente, use a data de hoje.\n- Se houver dia e mês sem ano, use o ano corrente.\n\nREGRAS DE TIPO:\n- \"entrada\" se o texto indicar crédito/recebimento: {recebido, crédito, creditado, estorno, reembolso, salário, depósito, ajuste a crédito}.\n- Caso contrário, \"saida\".\n- Se o valor vier com sinal negativo mas o contexto indicar PAGAMENTO na fatura do cartão (ajuste a crédito por exemplo) ou indicar estorno/reembolso, classifique como \"entrada\" e lance o valor positivo.\n\nREGRAS DE CATEGORIA (heurísticas, escolha UMA):\n- Viagem: hospedagem/hotel/pousada/airbnb/booking, tarifas de viagem, passagens.\n- Alimentação: restaurante, café, iFood, padaria, mercado com itens de consumo imediato; \"almoço\", \"jantar\", \"lanche\".\n- Transporte: Uber/99, combustível (posto), pedágio, estacionamento.\n- Saúde: farmácia, clínica, exames, plano de saúde.\n- Compras: varejo geral (Amazon, Shopee, Magazine Luiza, etc.).\n- Serviços: assinaturas/serviços digitais (Spotify, Netflix, Wasabi, Contabo, AWS), manutenção/serviço recorrente.\n- Educação: escola, curso, mensalidade educacional.\n- Moradia: aluguel, condomínio, luz, água, internet residencial.\n- Lazer: cinema, eventos, entretenimento não alimentar.\n- Cartão de Crédito: tarifas e encargos explícitos do cartão (anuidade/IOF) quando não se encaixar melhor em outra.\n- Dívidas/Empréstimos: parcelas/financiamentos/empréstimos bancários.\n- Receita: para entradas.\n- Outros: caso não se enquadre nas anteriores.\n\nREGRAS DE FILTRO:\n- Ignore linhas que sejam totais, rodapés, cabeçalhos ou descrições genéricas sem transação (ex.: \"Pagamentos Validos Normais\", \"Total da Fatura\").\n\nFORMATO FINAL:\n- Extraia TODAS as transações que encontrar na entrada.\n- Não inclua comentários, textos extras ou quebras indevidas — SOMENTE o JSON válido pedido.\n- Quando houver hospedagem na descrição, categorize como \"Viagem\".\n\nExemplo de saída:\n{\"transactions\":[\n {\"categoria\":\"Compras\",\"descricao\":\"Pet Love Clube\",\"valor\":12.06,\"data\":\"2025-11-04\",\"tipo\":\"saida\"},\n {\"categoria\":\"Serviços\",\"descricao\":\"Spotify\",\"valor\":40.90,\"data\":\"2025-11-04\",\"tipo\":\"saida\"}\n]}\n" | |
| } | |
| ] | |
| }, | |
| "builtInTools": {}, | |
| "options": {} | |
| }, | |
| "type": "@n8n/n8n-nodes-langchain.openAi", | |
| "typeVersion": 2, | |
| "position": [ | |
| -1408, | |
| -624 | |
| ], | |
| "id": "93d66d70-6ccd-4473-ac53-e3ab71275121", | |
| "name": "Message a model", | |
| "retryOnFail": true, | |
| "credentials": { | |
| "openAiApi": { | |
| "id": "ASEhNATa5Q5yMLGg", | |
| "name": "OpenAi account" | |
| } | |
| } | |
| }, | |
| { | |
| "parameters": { | |
| "assignments": { | |
| "assignments": [ | |
| { | |
| "id": "ad1ddc47-fe87-4364-95ae-c9945a2dd4cf", | |
| "name": "message", | |
| "value": "={{ $json.text }}", | |
| "type": "string" | |
| } | |
| ] | |
| }, | |
| "options": {} | |
| }, | |
| "type": "n8n-nodes-base.set", | |
| "typeVersion": 3.4, | |
| "position": [ | |
| -1744, | |
| -656 | |
| ], | |
| "id": "249efe3c-0b4d-4f59-9820-d1941062764b", | |
| "name": "Set Message from Audio" | |
| }, | |
| { | |
| "parameters": { | |
| "assignments": { | |
| "assignments": [ | |
| { | |
| "id": "6e1e69d7-6962-4646-a064-665ed8ea089e", | |
| "name": "message", | |
| "value": "={{ $json.message.text }}", | |
| "type": "string" | |
| } | |
| ] | |
| }, | |
| "options": {} | |
| }, | |
| "type": "n8n-nodes-base.set", | |
| "typeVersion": 3.4, | |
| "position": [ | |
| -1888, | |
| -880 | |
| ], | |
| "id": "439990a1-fc59-4de4-a3a7-96dcf4e9e56b", | |
| "name": "Set Message from Text" | |
| }, | |
| { | |
| "parameters": { | |
| "assignments": { | |
| "assignments": [ | |
| { | |
| "id": "3038539e-653b-40da-920b-69ea72ec3dc0", | |
| "name": "parsedJson", | |
| "value": "={{ JSON.parse($json.output[0].content[0].text) }}", | |
| "type": "object" | |
| } | |
| ] | |
| }, | |
| "options": {} | |
| }, | |
| "type": "n8n-nodes-base.set", | |
| "typeVersion": 3.4, | |
| "position": [ | |
| -928, | |
| -624 | |
| ], | |
| "id": "571a82bb-62c7-4abc-96dc-a40f9515364f", | |
| "name": "Parse AI JSON" | |
| }, | |
| { | |
| "parameters": { | |
| "conditions": { | |
| "options": { | |
| "caseSensitive": true, | |
| "leftValue": "", | |
| "typeValidation": "strict", | |
| "version": 2 | |
| }, | |
| "conditions": [ | |
| { | |
| "id": "fb611a02-1d8d-4726-b701-075738cbe52a", | |
| "leftValue": "={{ $json.parsedJson.transactions.length }}", | |
| "rightValue": 0, | |
| "operator": { | |
| "type": "number", | |
| "operation": "gt" | |
| } | |
| } | |
| ], | |
| "combinator": "and" | |
| }, | |
| "options": {} | |
| }, | |
| "type": "n8n-nodes-base.if", | |
| "typeVersion": 2.2, | |
| "position": [ | |
| -768, | |
| -592 | |
| ], | |
| "id": "124574b2-c1c3-4590-a255-0d003bba7222", | |
| "name": "Has transactions?" | |
| }, | |
| { | |
| "parameters": { | |
| "assignments": { | |
| "assignments": [ | |
| { | |
| "id": "73c5a33d-8319-4b29-b377-cb0888c272ce", | |
| "name": "message", | |
| "value": "Não consegui encontrar nenhuma transação no seu áudio/texto. 🙁", | |
| "type": "string" | |
| } | |
| ] | |
| }, | |
| "options": {} | |
| }, | |
| "type": "n8n-nodes-base.set", | |
| "typeVersion": 3.4, | |
| "position": [ | |
| 48, | |
| -480 | |
| ], | |
| "id": "168511b6-07b0-44e4-9adc-554874508983", | |
| "name": "Fail message" | |
| }, | |
| { | |
| "parameters": { | |
| "assignments": { | |
| "assignments": [ | |
| { | |
| "id": "73c5a33d-8319-4b29-b377-cb0888c272ce", | |
| "name": "message", | |
| "value": "=*{{ $json.user }}!* 👋\n\nSeu lançamento foi feito com sucesso:\n\n✅ *Tipo:* {{ $json.type }}\n🗓️ *Data:* {{ DateTime.fromISO($('Edit Fields').item.json.date).toFormat('dd/MM/yyyy') }}\n🧾 *Descrição:* {{ $json.description }}\n💰 *Valor:* R$ {{ $json.value.toLocaleString('pt-BR', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) }}\n🏷️ *Categoria:* {{ $json.category }}", | |
| "type": "string" | |
| } | |
| ] | |
| }, | |
| "options": {} | |
| }, | |
| "type": "n8n-nodes-base.set", | |
| "typeVersion": 3.4, | |
| "position": [ | |
| 80, | |
| -704 | |
| ], | |
| "id": "5fd4b4c1-1b8c-4dc0-b025-5f0627e15d04", | |
| "name": "Sucess message" | |
| }, | |
| { | |
| "parameters": { | |
| "dataTableId": { | |
| "__rl": true, | |
| "value": "rcz6CPiAZbvYcdSw", | |
| "mode": "list", | |
| "cachedResultName": "video", | |
| "cachedResultUrl": "/projects/Fk5PIHrixMIVGbVy/datatables/rcz6CPiAZbvYcdSw" | |
| }, | |
| "columns": { | |
| "mappingMode": "defineBelow", | |
| "value": { | |
| "value": "={{ $json.value }}", | |
| "date": "={{ $json.date }}", | |
| "type": "={{ $json.type }}", | |
| "category": "={{ $json.category }}", | |
| "description": "={{ $json.description }}", | |
| "user": "={{ $('Map Chat').item.json.user }}" | |
| }, | |
| "matchingColumns": [], | |
| "schema": [ | |
| { | |
| "id": "value", | |
| "displayName": "value", | |
| "required": false, | |
| "defaultMatch": false, | |
| "display": true, | |
| "type": "number", | |
| "readOnly": false, | |
| "removed": false | |
| }, | |
| { | |
| "id": "date", | |
| "displayName": "date", | |
| "required": false, | |
| "defaultMatch": false, | |
| "display": true, | |
| "type": "dateTime", | |
| "readOnly": false, | |
| "removed": false | |
| }, | |
| { | |
| "id": "type", | |
| "displayName": "type", | |
| "required": false, | |
| "defaultMatch": false, | |
| "display": true, | |
| "type": "string", | |
| "readOnly": false, | |
| "removed": false | |
| }, | |
| { | |
| "id": "category", | |
| "displayName": "category", | |
| "required": false, | |
| "defaultMatch": false, | |
| "display": true, | |
| "type": "string", | |
| "readOnly": false, | |
| "removed": false | |
| }, | |
| { | |
| "id": "description", | |
| "displayName": "description", | |
| "required": false, | |
| "defaultMatch": false, | |
| "display": true, | |
| "type": "string", | |
| "readOnly": false, | |
| "removed": false | |
| }, | |
| { | |
| "id": "user", | |
| "displayName": "user", | |
| "required": false, | |
| "defaultMatch": false, | |
| "display": true, | |
| "type": "string", | |
| "readOnly": false, | |
| "removed": false | |
| } | |
| ], | |
| "attemptToConvertTypes": false, | |
| "convertFieldsToString": false | |
| }, | |
| "options": {} | |
| }, | |
| "type": "n8n-nodes-base.dataTable", | |
| "typeVersion": 1, | |
| "position": [ | |
| -48, | |
| -704 | |
| ], | |
| "id": "d119a50b-1760-4533-b506-974992f8b3ca", | |
| "name": "Save" | |
| }, | |
| { | |
| "parameters": { | |
| "content": "## Receive message", | |
| "height": 272, | |
| "width": 368, | |
| "color": 5 | |
| }, | |
| "type": "n8n-nodes-base.stickyNote", | |
| "position": [ | |
| -2816, | |
| -768 | |
| ], | |
| "typeVersion": 1, | |
| "id": "e6344816-956a-493f-a33d-3f2aa83c7321", | |
| "name": "Sticky Note" | |
| }, | |
| { | |
| "parameters": { | |
| "content": "## Extract informations", | |
| "height": 1008, | |
| "width": 1440, | |
| "color": 2 | |
| }, | |
| "type": "n8n-nodes-base.stickyNote", | |
| "position": [ | |
| -2432, | |
| -1056 | |
| ], | |
| "typeVersion": 1, | |
| "id": "7cb26b1b-2330-48ef-a995-4b658f2da0f6", | |
| "name": "Sticky Note1" | |
| }, | |
| { | |
| "parameters": { | |
| "content": "## Store and response to telegram", | |
| "height": 576, | |
| "width": 1536, | |
| "color": 4 | |
| }, | |
| "type": "n8n-nodes-base.stickyNote", | |
| "position": [ | |
| -976, | |
| -912 | |
| ], | |
| "typeVersion": 1, | |
| "id": "1bae0ed3-36f1-43f9-85ac-befc9d150747", | |
| "name": "Sticky Note2" | |
| }, | |
| { | |
| "parameters": { | |
| "fieldToSplitOut": "parsedJson.transactions", | |
| "options": {} | |
| }, | |
| "type": "n8n-nodes-base.splitOut", | |
| "typeVersion": 1, | |
| "position": [ | |
| -624, | |
| -752 | |
| ], | |
| "id": "a078b6db-bb12-4c13-a0c2-07d2d9f9ef2f", | |
| "name": "Split Out" | |
| }, | |
| { | |
| "parameters": { | |
| "assignments": { | |
| "assignments": [ | |
| { | |
| "id": "a3ba42dc-84ea-4833-acd8-e77f4f7d4ddf", | |
| "name": "type", | |
| "value": "={{ $json.tipo }}", | |
| "type": "string" | |
| }, | |
| { | |
| "id": "fe50a7bf-d43c-43b0-bb2d-c0d9ac9cd909", | |
| "name": "date", | |
| "value": "={{ $json.data }}", | |
| "type": "string" | |
| }, | |
| { | |
| "id": "f9afcd79-07ca-47fb-88f9-11b358e58417", | |
| "name": "description", | |
| "value": "={{ $json.descricao }}", | |
| "type": "string" | |
| }, | |
| { | |
| "id": "2b5aa2b9-7349-4f6d-91e0-ec34e2416be7", | |
| "name": "value", | |
| "value": "={{ $json.valor }}", | |
| "type": "string" | |
| }, | |
| { | |
| "id": "3c5215ad-b557-4772-a5b2-cc1997ae142b", | |
| "name": "category", | |
| "value": "={{ $json.categoria }}", | |
| "type": "string" | |
| } | |
| ] | |
| }, | |
| "options": {} | |
| }, | |
| "type": "n8n-nodes-base.set", | |
| "typeVersion": 3.4, | |
| "position": [ | |
| -192, | |
| -704 | |
| ], | |
| "id": "c31405d8-08a3-4812-a1ad-63448098577e", | |
| "name": "Edit Fields" | |
| }, | |
| { | |
| "parameters": { | |
| "options": {} | |
| }, | |
| "type": "n8n-nodes-base.splitInBatches", | |
| "typeVersion": 3, | |
| "position": [ | |
| -384, | |
| -768 | |
| ], | |
| "id": "f6f84425-e963-4496-97eb-b7d6ab5ef12e", | |
| "name": "Loop Over Items" | |
| }, | |
| { | |
| "parameters": { | |
| "conditions": { | |
| "options": { | |
| "caseSensitive": true, | |
| "leftValue": "", | |
| "typeValidation": "strict", | |
| "version": 2 | |
| }, | |
| "conditions": [ | |
| { | |
| "id": "42bd45d0-768b-4c47-bb1b-fdc9f5480025", | |
| "leftValue": "={{ $json.message.document.mime_type }}", | |
| "rightValue": "csv", | |
| "operator": { | |
| "type": "string", | |
| "operation": "contains" | |
| } | |
| }, | |
| { | |
| "id": "12f09a33-f1b2-435b-88bf-0f593d50159f", | |
| "leftValue": "={{ $json.message.document.file_name }}", | |
| "rightValue": "csv", | |
| "operator": { | |
| "type": "string", | |
| "operation": "endsWith" | |
| } | |
| } | |
| ], | |
| "combinator": "or" | |
| }, | |
| "options": {} | |
| }, | |
| "type": "n8n-nodes-base.if", | |
| "typeVersion": 2.2, | |
| "position": [ | |
| -2160, | |
| -432 | |
| ], | |
| "id": "da8111a7-6770-406b-9405-4c5f7c10b6d3", | |
| "name": "If" | |
| }, | |
| { | |
| "parameters": { | |
| "resource": "file", | |
| "fileId": "={{ $('Telegram Trigger').item.json.message.document.file_id }}", | |
| "additionalFields": {} | |
| }, | |
| "type": "n8n-nodes-base.telegram", | |
| "typeVersion": 1.2, | |
| "position": [ | |
| -1952, | |
| -448 | |
| ], | |
| "id": "dc9b8143-abf7-44ae-8b15-00c436900781", | |
| "name": "Get CSV", | |
| "webhookId": "bf319801-62af-4941-bd7b-c4487574477e", | |
| "credentials": { | |
| "telegramApi": { | |
| "id": "sTNb3pmq5jE43YMZ", | |
| "name": "Telegram account" | |
| } | |
| } | |
| }, | |
| { | |
| "parameters": { | |
| "resource": "file", | |
| "fileId": "={{ $('Telegram Trigger').item.json.message.voice.file_id }}", | |
| "additionalFields": {} | |
| }, | |
| "type": "n8n-nodes-base.telegram", | |
| "typeVersion": 1.2, | |
| "position": [ | |
| -2032, | |
| -656 | |
| ], | |
| "id": "76aa214a-c682-4a73-9a80-f7cfef756c63", | |
| "name": "Get audio", | |
| "webhookId": "bf319801-62af-4941-bd7b-c4487574477e", | |
| "credentials": { | |
| "telegramApi": { | |
| "id": "sTNb3pmq5jE43YMZ", | |
| "name": "Telegram account" | |
| } | |
| } | |
| }, | |
| { | |
| "parameters": { | |
| "options": { | |
| "rawData": true, | |
| "readAsString": true | |
| } | |
| }, | |
| "id": "1860907f-6abe-4fee-8935-8afd0a0ecc04", | |
| "name": "Convert To Spreadsheet", | |
| "type": "n8n-nodes-base.spreadsheetFile", | |
| "position": [ | |
| -1792, | |
| -448 | |
| ], | |
| "typeVersion": 1 | |
| }, | |
| { | |
| "parameters": { | |
| "jsCode": "// ======= Configuração =======\nconst FIELD_CANDIDATES = {\n desc: ['Estabelecimento','estabelecimento','descricao','Descrição','title','Title','merchant','nome','estab'],\n amount: ['Valor','valor','valor_total','amount','Amount','price'],\n date: ['Data','data','date','Date'] // não usado na frase (usamos \"hoje\"), mas deixado para futura expansão\n};\n\nconst IGNORE_DESCRIPTIONS = [\n 'pagamentos validos normais',\n 'pagamento válido normal',\n 'pagamento recebido'\n];\n\n// ======= Helpers =======\nconst items = await $input.all();\n\nconst fmtHoje = new Intl.DateTimeFormat('pt-BR', { timeZone: 'America/Sao_Paulo' });\nconst hojeBR = fmtHoje.format(new Date());\n\n// remove BOM/acentos/caixa e normaliza espaços\nfunction normalizeKey(k){\n return String(k||'')\n .replace(/\\uFEFF/g,'')\n .normalize('NFD').replace(/[\\u0300-\\u036f]/g,'')\n .toLowerCase().trim();\n}\nfunction normalizeText(s){\n return String(s||'')\n .replace(/\\uFEFF/g,'')\n .replace(/\\*/g,' ') // remove asteriscos\n .replace(/\\s+/g,' ') // compacta espaços\n .trim();\n}\nfunction normForCompare(s){\n return normalizeText(s)\n .normalize('NFD').replace(/[\\u0300-\\u036f]/g,'')\n .toLowerCase();\n}\nfunction toTitleCase(s){\n return normalizeText(s).toLowerCase().replace(/\\b([a-zà-ú])([a-zà-ú]*)/gi, (_,a,b)=> a.toUpperCase()+b);\n}\n\nfunction pick(obj, candidates){\n // tenta direto\n for (const c of candidates) if (obj[c] !== undefined) return obj[c];\n // tenta por chaves normalizadas\n const map = new Map(Object.keys(obj).map(k => [normalizeKey(k), k]));\n for (const c of candidates){\n const nk = normalizeKey(c);\n if (map.has(nk)) return obj[map.get(nk)];\n }\n return undefined;\n}\n\n// \"R$ 3.032,81\" -> 3032.81 ; \"3032.81\" -> 3032.81 ; \"3.032,81\" -> 3032.81\nfunction parseAmountAny(s){\n if (s == null) return 0;\n const str = String(s).trim();\n // se tem vírgula como decimal (pt-BR)\n if (/,/.test(str) && !/^\\d+(\\.\\d+)?$/.test(str)){\n const cleaned = str.replace(/[R$\\s]/g,'').replace(/\\./g,'').replace(',', '.');\n const n = Number(cleaned);\n return Number.isFinite(n) ? n : 0;\n }\n // caso \"Nubank\" já venha número (ou string en-US)\n const n = Number(str.replace(/[R$\\s]/g,''));\n return Number.isFinite(n) ? n : 0;\n}\n\n// ======= Processamento =======\nconst lines = [];\n\nfor (const it of items){\n const row = it.json ?? it;\n\n const rawDesc = pick(row, FIELD_CANDIDATES.desc);\n const rawAmount = pick(row, FIELD_CANDIDATES.amount);\n\n const desc = toTitleCase(rawDesc || '');\n const descCmp = normForCompare(desc);\n\n // ignorar linhas específicas\n if (IGNORE_DESCRIPTIONS.includes(descCmp)) continue;\n\n const amount = parseAmountAny(rawAmount);\n if (!desc || !Number.isFinite(amount)) continue;\n\n const valorBR = Math.abs(amount).toLocaleString('pt-BR', { minimumFractionDigits:2, maximumFractionDigits:2 });\n lines.push(`Compra de ${valorBR} no ${desc} na data de ${hojeBR}`);\n}\n\n// ======= Saída preservando contexto =======\nconst baseBinary = items[0]?.binary ? { ...items[0].binary } : undefined;\n\nconst out = {\n json: { message: lines.join('\\n') },\n pairedItem: items.map((_, i) => ({ item: i }))\n};\nif (baseBinary) out.binary = baseBinary;\n\nreturn [out];\n" | |
| }, | |
| "type": "n8n-nodes-base.code", | |
| "typeVersion": 2, | |
| "position": [ | |
| -1616, | |
| -448 | |
| ], | |
| "id": "a07ac806-c9f1-49ef-8785-7a51bd7cbff3", | |
| "name": "Code in JavaScript" | |
| }, | |
| { | |
| "parameters": { | |
| "jsCode": "const items = await $input.all();\n\nfunction pickName(from) {\n if (!from) return '';\n // tenta username; se não houver, usa \"first_name last_name\" (quando houver)\n const fname = from.first_name;\n return fname;\n}\n\nreturn items.map((it, idx) => {\n const j = it.json ?? it;\n\n const message = j.message ?? j; // aceita quando já está \"achatado\"\n const chat = message.chat ?? j.chat ?? {};\n const from = message.from ?? j.from ?? {};\n\n const chatId = chat.id ?? null;\n const chatType = chat.type ?? null;\n const chatTitle = chat.title ?? null;\n const fromId = from.id ?? null;\n const userLabel = pickName(from);\n\n return {\n json: {\n ...j,\n chat_id: chatId,\n chat_type: chatType,\n chat_title: chatTitle,\n from_id: fromId,\n user: userLabel\n },\n binary: it.binary,\n pairedItem: { item: idx }\n };\n});\n" | |
| }, | |
| "type": "n8n-nodes-base.code", | |
| "typeVersion": 2, | |
| "position": [ | |
| -2608, | |
| -704 | |
| ], | |
| "id": "ec97b8f8-a3e6-49d5-b752-c3e699eb6eac", | |
| "name": "Map Chat" | |
| }, | |
| { | |
| "parameters": { | |
| "chatId": "={{ $('Map Chat').item.json.chat_id }}", | |
| "text": "={{ $json.message }}", | |
| "additionalFields": { | |
| "appendAttribution": false | |
| } | |
| }, | |
| "type": "n8n-nodes-base.telegram", | |
| "typeVersion": 1.2, | |
| "position": [ | |
| 368, | |
| -720 | |
| ], | |
| "id": "e1611863-a363-4687-9a74-5572bcdcddad", | |
| "name": "Send message", | |
| "webhookId": "d6d8c013-7f6f-4456-9c9e-894738f241c9", | |
| "credentials": { | |
| "telegramApi": { | |
| "id": "sTNb3pmq5jE43YMZ", | |
| "name": "Telegram account" | |
| } | |
| } | |
| }, | |
| { | |
| "parameters": { | |
| "jsCode": "const items = await $input.all();\n\n// Coleta todas as mensagens\nconst msgs = items.map(i => i.json?.message).filter(Boolean);\n\n// Junta respeitando limite do Telegram\nconst MAX = 3800;\nconst out = [];\nlet bucket = \"\";\n\nfor (const m of msgs) {\n const piece = (bucket ? \"\\n\\n\" : \"\") + m;\n if ((bucket.length + piece.length) > MAX) {\n out.push({ json: { message: bucket } });\n bucket = m;\n } else {\n bucket += piece;\n }\n}\nif (bucket) out.push({ json: { message: bucket } });\n\nreturn out.map(o => ({\n ...o,\n pairedItem: [{ item: 0 }]\n}));\n" | |
| }, | |
| "type": "n8n-nodes-base.code", | |
| "typeVersion": 2, | |
| "position": [ | |
| -192, | |
| -864 | |
| ], | |
| "id": "a8020ed1-f874-4d59-b864-8d5a31034456", | |
| "name": "Aggregate" | |
| }, | |
| { | |
| "parameters": { | |
| "content": "## Retrive data and generate HTML view", | |
| "height": 416, | |
| "width": 1376, | |
| "color": 7 | |
| }, | |
| "type": "n8n-nodes-base.stickyNote", | |
| "position": [ | |
| -2832, | |
| 64 | |
| ], | |
| "typeVersion": 1, | |
| "id": "34cb849c-8277-4799-9da0-4ddf0971a108", | |
| "name": "Sticky Note3" | |
| }, | |
| { | |
| "parameters": { | |
| "path": "finances-video", | |
| "responseMode": "responseNode", | |
| "options": {} | |
| }, | |
| "type": "n8n-nodes-base.webhook", | |
| "typeVersion": 2.1, | |
| "position": [ | |
| -2800, | |
| 224 | |
| ], | |
| "id": "ba31b11d-b969-4736-b21c-3b3004006057", | |
| "name": "Webhook", | |
| "webhookId": "e3067d29-7457-4335-ba08-57aebae41697" | |
| }, | |
| { | |
| "parameters": { | |
| "operation": "get", | |
| "dataTableId": { | |
| "__rl": true, | |
| "value": "rcz6CPiAZbvYcdSw", | |
| "mode": "list", | |
| "cachedResultName": "video", | |
| "cachedResultUrl": "/projects/Fk5PIHrixMIVGbVy/datatables/rcz6CPiAZbvYcdSw" | |
| }, | |
| "filters": { | |
| "conditions": [ | |
| { | |
| "keyName": "date", | |
| "condition": "gte", | |
| "keyValue": "={{ $json.start }}" | |
| }, | |
| { | |
| "keyName": "date", | |
| "condition": "lte", | |
| "keyValue": "={{ $json.end }}" | |
| } | |
| ] | |
| }, | |
| "returnAll": true | |
| }, | |
| "name": "List Finance (Data Table)", | |
| "type": "n8n-nodes-base.dataTable", | |
| "typeVersion": 1, | |
| "position": [ | |
| -2288, | |
| 224 | |
| ], | |
| "id": "e5fd6190-dc90-47eb-a766-78e48c510dde", | |
| "alwaysOutputData": true | |
| }, | |
| { | |
| "parameters": { | |
| "jsCode": "// ================= helpers =================\nfunction isEntrada(t){ const v=(t||'').toLowerCase(); return ['entrada','receita','salario','salário','recebido','recebidos'].some(x=>v.includes(x)); }\nfunction isSaida(t){ const v=(t||'').toLowerCase(); return ['saida','saída','despesa','despesas','compra','custo'].some(x=>v.includes(x)); }\nfunction brl(x){ return (Number(x)||0).toLocaleString('pt-BR',{minimumFractionDigits:2, maximumFractionDigits:2}); }\nfunction normDate(s){ if(!s) return null; if(/^\\d{4}-\\d{2}$/.test(s)) return s + '-01'; return s; }\n\n// NÃO CONVERTE PARA Date: usa só a string YYYY-MM-DD\nfunction ymdToBR(ymd){\n const s = (ymd||'').slice(0,10);\n const [y,m,d] = s.split('-');\n return (y && m && d) ? `${d}/${m}/${y}` : '';\n}\n\nfunction ptMonthYear(ym){\n const [y, m] = (ym||'').split('-').map(Number);\n // Representação estática sem usar Date: mês por nome em pt-BR\n const meses = ['janeiro','fevereiro','março','abril','maio','junho','julho','agosto','setembro','outubro','novembro','dezembro'];\n if (!y || !m) return ym || '';\n return `${meses[m-1]} de ${y}`;\n}\n\n// ================ inputs =====================\nconst datesItem = $items('Set Infos', 0, 0)[0]?.json || {};\nconst fromStrIn = datesItem.start;\nconst toStrIn = datesItem.end;\nconst initialBalance = Number(datesItem.initialBalance ?? datesItem.ib ?? 0) || 0;\n\nconst from = normDate(fromStrIn);\nconst to = normDate(toStrIn) || new Date().toISOString().slice(0,10);\n\n// Limites em STRING (sem timezone)\nconst fromY = (from || '0000-00-00').slice(0,10);\nconst toY = (to || '9999-12-31').slice(0,10);\n\nif (!fromY || fromY === '0000-00-00') {\n return [{ json: { html_content: `<!doctype html><meta charset=\"utf-8\"><meta name=\"color-scheme\" content=\"light\"><body style=\"font:14px system-ui;background:#fff;color:#111\"><h3>Parâmetro ?start=AAAA-MM-DD (ou AAAA-MM) é obrigatório</h3></body>` } }];\n}\n\n// rows de entrada (Data Table -> Code)\nconst rows = (await $input.all()).map(i => i.json);\n\n// normaliza (sem parse de data)\nconst normalized = rows.map(r=>({\n date: r.date, // mantém original\n ymd: (r.date||'').slice(0,10), // YYYY-MM-DD (base para tudo)\n ym: (r.date||'').slice(0,7), // YYYY-MM\n value: Number(r.value)||0,\n type: r.type||'',\n description: r.description||'',\n category: r.category||'',\n user: r.user || ''\n}));\n\n// filtra por string (evita timezone)\nconst inRange = normalized.filter(r => r.ymd && r.ymd >= fromY && r.ymd <= toY);\n\n// meses presentes\nconst months = Array.from(new Set(inRange.map(x=>x.ym))).filter(Boolean).sort();\n\n// agregação mensal\nconst perMonth = months.map(m=>{\n const xs = inRange.filter(x=>x.ym===m);\n const recebidos = xs.filter(x=>isEntrada(x.type)).reduce((a,b)=>a+b.value,0);\n const despesas = xs.filter(x=>isSaida(x.type)).reduce((a,b)=>a+b.value,0);\n return { month:m, recebidos, despesas, saldo: recebidos - despesas };\n});\n\n// totais do período\nconst totalRecebidos = inRange.filter(x=>isEntrada(x.type)).reduce((a,b)=>a+b.value,0);\nconst totalDespesas = inRange.filter(x=>isSaida(x.type)).reduce((a,b)=>a+b.value,0);\nconst saldoPeriodo = totalRecebidos - totalDespesas;\n\n// —— saldo acumulado (sem converter datas) ——\nconst timeline = inRange\n .map(x => ({ ...x, delta: isEntrada(x.type) ? Number(x.value||0) : -Number(x.value||0) }))\n .sort((a,b) => a.ymd.localeCompare(b.ymd) || a.description.localeCompare(b.description));\n\nlet running = initialBalance;\nconst saldoAcumSeries = timeline.map(t => {\n running += t.delta;\n return { ymd: t.ymd, label: ymdToBR(t.ymd), balance: Number(running.toFixed(2)) };\n});\nconst saldoInicial = initialBalance;\nconst saldoFinal = Number((initialBalance + saldoPeriodo).toFixed(2));\n\n// ---- séries linha (recebidos×despesas) ----\nlet lineLabels = [];\nlet serieRecebidos = [];\nlet serieDespesas = [];\nconst isSingleMonth = months.length === 1;\n\nif (isSingleMonth) {\n const days = Array.from(new Set(inRange.map(x=>x.ymd))).sort(); // YYYY-MM-DD\n lineLabels = days.map(d => ymdToBR(d));\n const recMap = new Map(), desMap = new Map();\n for (const d of days) { recMap.set(d, 0); desMap.set(d, 0); }\n for (const it of inRange) {\n if (isEntrada(it.type)) recMap.set(it.ymd, (recMap.get(it.ymd) || 0) + it.value);\n else if (isSaida(it.type)) desMap.set(it.ymd, (desMap.get(it.ymd) || 0) + it.value);\n }\n serieRecebidos = days.map(d => Number((recMap.get(d)||0).toFixed(2)));\n serieDespesas = days.map(d => Number((desMap.get(d)||0).toFixed(2)));\n} else {\n lineLabels = perMonth.map(x=> ptMonthYear(x.month));\n serieRecebidos = perMonth.map(x=> Number(x.recebidos.toFixed(2)));\n serieDespesas = perMonth.map(x=> Number(x.despesas.toFixed(2)));\n}\n\n// ---- gráfico da direita (dinâmico) ----\nlet rightLabels, rightData, rightTitle;\nif (isSingleMonth) {\n rightLabels = ['Receitas', 'Despesas'];\n rightData = [Number(totalRecebidos.toFixed(2)), Number(totalDespesas.toFixed(2))];\n rightTitle = 'Receitas × Despesas';\n} else {\n rightLabels = perMonth.map(x => ptMonthYear(x.month));\n rightData = perMonth.map(x => Number(x.saldo.toFixed(2)));\n rightTitle = 'Saldo por mês';\n}\n\n// —— despesas por categoria (barras) ——\nconst catMap = new Map();\nfor (const r of inRange) {\n if (isSaida(r.type)) {\n const k = r.category || 'Sem categoria';\n catMap.set(k, (catMap.get(k) || 0) + (Number(r.value) || 0));\n }\n}\nconst catLabels = Array.from(catMap.keys());\nconst catValues = Array.from(catMap.values());\nconst catTitle = `Despesas por categoria ${isSingleMonth ? '(do mês)' : '(no período)'}`;\n\n// tabela saldos por mês\nconst saldoRows = perMonth.map(x =>\n `<tr>\n <td>${ptMonthYear(x.month)}</td>\n <td class=\"td-right\">R$ ${brl(x.recebidos)}</td>\n <td class=\"td-right\">R$ ${brl(x.despesas)}</td>\n <td class=\"td-right\" style=\"font-weight:600;color:${x.saldo>=0?'#1a7f37':'#c62828'}\">R$ ${brl(x.saldo)}</td>\n </tr>`\n).join('');\n\n// lançamentos (ordenados por data ASC)\nconst lancamentos = [...inRange].sort((a,b)=> a.ymd.localeCompare(b.ymd) || a.description.localeCompare(b.description));\nconst lancRows = lancamentos.map(x=>{\n const isIn = isEntrada(x.type);\n const color = isIn ? '#1a7f37' : '#c62828';\n const sign = isIn ? '+' : '-';\n return `<tr>\n <td>${ymdToBR(x.ymd)}</td>\n <td>${x.user || ''}</td>\n <td>${x.description || ''}</td>\n <td>${x.category || ''}</td>\n <td class=\"td-right\" style=\"color:${color};font-weight:600\">${sign} R$ ${brl(x.value)}</td>\n </tr>`;\n}).join('');\n\n// =================== HTML =====================\nconst html = `<!doctype html>\n<html lang=\"pt-BR\">\n<head>\n<meta charset=\"utf-8\" />\n<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\" />\n<meta name=\"color-scheme\" content=\"light\">\n<title>Relatório Financeiro (${fromY} a ${toY})</title>\n<style>\n :root{\n --bg:#ffffff; --card:#ffffff; --text:#0b1220; --muted:#5f6b7a; --border:#e6eaf2;\n --pos:#1a7f37; --neg:#c62828; --shadow:0 8px 20px rgba(0,0,0,.06); --radius:14px; --wrap:1080px;\n }\n *{box-sizing:border-box}\n html,body{margin:0; background:#ffffff; color:var(--text); font:14px/1.45 ui-sans-serif,system-ui,Inter,Roboto,Arial;}\n .wrap{max-width:var(--wrap); margin:32px auto; padding:0 20px;}\n header{display:flex; align-items:center; justify-content:space-between; gap:16px; margin-bottom:18px;}\n h1{margin:0; font-weight:700; font-size:26px; letter-spacing:.2px;}\n .badge{display:inline-flex; gap:8px; align-items:center; padding:8px 12px; border:1px solid var(--border); border-radius:999px; background:#f3f4f6; color:#374151; font-weight:700;}\n .cards{display:grid;grid-template-columns:repeat(12,1fr); gap:16px; margin:18px 0;}\n .card{grid-column:span 12; background:var(--card); border:1px solid var(--border); border-radius:var(--radius); box-shadow:var(--shadow); padding:16px;}\n .kpis{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr)); gap:14px;}\n .kpi{background:var(--card); border:1px solid var(--border); border-radius:12px; padding:14px;}\n .kpi .label{color:var(--muted); font-size:12px;}\n .kpi .value{margin-top:6px; font-weight:800; font-size:22px;}\n .chart-2col{display:grid; grid-template-columns: 1fr 1fr; gap:16px;}\n .chart-box{border:1px solid var(--border); border-radius:12px; padding:12px; background:var(--card);}\n .chart-title{margin:0 0 10px 0; font-weight:700; font-size:16px;}\n @media (max-width: 900px){ .chart-2col{grid-template-columns:1fr} }\n\n .table-wrap{overflow:auto; border:1px solid var(--border); border-radius:12px; background:var(--card);}\n table{width:100%; border-collapse:separate; border-spacing:0; min-width:720px;}\n thead th{position:sticky; top:0; background:#ffffff; color:var(--text); text-align:left; z-index:1; border-bottom:1px solid var(--border);}\n th, td{padding:10px 12px;}\n tbody tr:nth-child(odd){background-color:#00000005;}\n tbody tr:hover{background:#00000008;}\n .td-right{text-align:right;}\n</style>\n</head>\n<body>\n <div class=\"wrap\">\n <header>\n <h1>Relatório Financeiro</h1>\n <span class=\"badge\">Período: ${fromY} → ${toY}</span>\n </header>\n\n <section class=\"cards\">\n <div class=\"card\">\n <div class=\"kpis\">\n <div class=\"kpi\"><div class=\"label\">Saldo Inicial</div><div class=\"value\">R$ ${brl(saldoInicial)}</div></div>\n <div class=\"kpi\"><div class=\"label\">Total Recebidos</div><div class=\"value\" style=\"color:var(--pos)\">R$ ${brl(totalRecebidos)}</div></div>\n <div class=\"kpi\"><div class=\"label\">Total Despesas</div><div class=\"value\" style=\"color:var(--neg)\">R$ ${brl(totalDespesas)}</div></div>\n <div class=\"kpi\"><div class=\"label\">Saldo no Período</div><div class=\"value\" style=\"color:${saldoPeriodo>=0?'var(--pos)':'var(--neg)'}\">R$ ${brl(saldoPeriodo)}</div></div>\n <div class=\"kpi\"><div class=\"label\">Saldo Final</div><div class=\"value\" style=\"color:${saldoFinal>=0?'var(--pos)':'var(--neg)'}\">R$ ${brl(saldoFinal)}</div></div>\n </div>\n </div>\n\n <div class=\"card\">\n <div class=\"chart-2col\">\n <div class=\"chart-box\">\n <h3 class=\"chart-title\">${isSingleMonth ? 'Recebidos × Despesas por dia' : 'Recebidos × Despesas por mês'}</h3>\n <div style=\"height:260px\"><canvas id=\"chartLine\"></canvas></div>\n </div>\n <div class=\"chart-box\">\n <h3 class=\"chart-title\">${isSingleMonth ? 'Receitas × Despesas' : 'Saldo por mês'}</h3>\n <div style=\"height:260px\"><canvas id=\"chartRight\"></canvas></div>\n </div>\n </div>\n </div>\n\n <div class=\"card\">\n <h3 class=\"chart-title\">${catTitle}</h3>\n ${\n catLabels.length\n ? `<div style=\"height:260px\"><canvas id=\"chartCat\"></canvas></div>`\n : `<div style=\"padding:8px;color:#5f6b7a\">Sem despesas no período.</div>`\n }\n </div>\n\n <div class=\"card\">\n <h3 class=\"chart-title\">Saldo por mês</h3>\n <div class=\"table-wrap\">\n <table>\n <thead><tr><th>Mês</th><th class=\"td-right\">Recebidos</th><th class=\"td-right\">Despesas</th><th class=\"td-right\">Saldo</th></tr></thead>\n <tbody>${perMonth.length ? saldoRows : '<tr><td colspan=\"4\" style=\"padding:16px;color:#5f6b7a\">Sem dados no período.</td></tr>'}</tbody>\n </table>\n </div>\n </div>\n\n <div class=\"card\">\n <h3 class=\"chart-title\">Lançamentos</h3>\n <div class=\"table-wrap\">\n <table>\n <thead><tr><th>Data</th><th>Quem</th><th>Descrição</th><th>Categoria</th><th class=\"td-right\">Valor</th></tr></thead>\n <tbody>${lancRows || '<tr><td colspan=\"5\" style=\"padding:16px;color:#5f6b7a\">Sem lançamentos.</td></tr>'}</tbody>\n </table>\n </div>\n </div>\n </section>\n </div>\n\n<script src=\"https://cdn.jsdelivr.net/npm/chart.js\"><\\/script>\n<script>\n(() => {\n if (!window.Chart) return;\n\n Chart.defaults.color = '#0b1220';\n const grid = '#e6eaf2';\n const tick = '#5f6b7a';\n Chart.defaults.scales = Chart.defaults.scales || {};\n Chart.defaults.scales.x = { grid: { color: grid }, ticks: { color: tick } };\n Chart.defaults.scales.y = { grid: { color: grid }, ticks: { color: tick }, beginAtZero:true };\n\n // --- line (Recebidos × Despesas) ---\n const lineCtx = document.getElementById('chartLine').getContext('2d');\n new Chart(lineCtx, {\n type: 'line',\n data: {\n labels: ${JSON.stringify(lineLabels)},\n datasets: [\n { label: 'Recebidos', data: ${JSON.stringify(serieRecebidos)}, borderWidth: 2, tension: 0.25, pointRadius: 3 },\n { label: 'Despesas', data: ${JSON.stringify(serieDespesas)}, borderWidth: 2, tension: 0.25, pointRadius: 3 }\n ]\n },\n options: {\n responsive: true,\n maintainAspectRatio: false,\n plugins: { legend: { position: 'bottom', labels: { color: '#0b1220' } } },\n scales: {\n x:{ grid:{ color: grid }, ticks:{ color: tick } },\n y:{ grid:{ color: grid }, ticks:{ color: tick }, beginAtZero:true }\n }\n }\n });\n\n // --- right (pizza 1 mês; barras empilhadas >1 mês) ---\n const rightCtx = document.getElementById('chartRight').getContext('2d');\n const isSingle = ${JSON.stringify(isSingleMonth)};\n const labelsRight = ${JSON.stringify(rightLabels)};\n const dataArrRight = ${JSON.stringify(rightData)};\n\n const baseOptions = {\n responsive: true,\n maintainAspectRatio: false,\n plugins: { legend: { position: 'bottom', labels: { color: '#0b1220' } } }\n };\n\n if (isSingle) {\n new Chart(rightCtx, {\n type: 'pie',\n data: { labels: labelsRight, datasets: [{ data: dataArrRight }] },\n options: baseOptions\n });\n } else {\n const positivos = dataArrRight.map(v => v > 0 ? v : 0);\n const negativos = dataArrRight.map(v => v < 0 ? v : 0);\n\n new Chart(rightCtx, {\n type: 'bar',\n data: {\n labels: labelsRight,\n datasets: [\n { label: 'Positivo', data: positivos, backgroundColor: 'rgba(26,127,55,0.25)', borderColor: '#1a7f37', borderWidth: 1, stack: 'saldo' },\n { label: 'Negativo', data: negativos, backgroundColor: 'rgba(198,40,40,0.20)', borderColor: '#c62828', borderWidth: 1, stack: 'saldo' }\n ]\n },\n options: {\n ...baseOptions,\n scales: {\n x: { stacked: true, grid: { color: grid }, ticks: { color: tick } },\n y: { stacked: true, grid: { color: grid }, ticks: { color: tick } }\n }\n }\n });\n }\n\n // --- despesas por categoria (barras) ---\n const catLabels = ${JSON.stringify(catLabels)};\n const catValues = ${JSON.stringify(catValues)};\n if (catLabels.length) {\n const catCtx = document.getElementById('chartCat').getContext('2d');\n new Chart(catCtx, {\n type: 'bar',\n data: {\n labels: catLabels,\n datasets: [\n { label: 'Despesas', data: catValues, borderWidth: 1 }\n ]\n },\n options: {\n responsive: true,\n maintainAspectRatio: false,\n plugins: { legend: { position: 'bottom', labels: { color: '#0b1220' } } },\n scales: {\n x: { grid: { color: grid }, ticks: { color: tick } },\n y: { grid: { color: grid }, ticks: { color: tick }, beginAtZero: true }\n }\n }\n });\n }\n})();\n<\\/script>\n</body>\n</html>`;\n\nreturn [{ json: { html_content: html } }];\n" | |
| }, | |
| "name": "Generate HTML", | |
| "type": "n8n-nodes-base.code", | |
| "typeVersion": 2, | |
| "position": [ | |
| -2032, | |
| 224 | |
| ], | |
| "id": "6434bb32-7f20-4e61-bc44-c6874c0219f5" | |
| }, | |
| { | |
| "parameters": { | |
| "keepOnlySet": true, | |
| "values": { | |
| "string": [ | |
| { | |
| "name": "start", | |
| "value": "={{ $json.query.start ?? '2024-01-01' }}" | |
| }, | |
| { | |
| "name": "end", | |
| "value": "={{ $json.query.end ?? (new Date().toISOString().slice(0,10)) }}" | |
| } | |
| ], | |
| "number": [ | |
| { | |
| "name": "initialBalance", | |
| "value": "={{ Number($json.query.ib) ?? 0 }}" | |
| } | |
| ] | |
| }, | |
| "options": {} | |
| }, | |
| "name": "Set Infos", | |
| "type": "n8n-nodes-base.set", | |
| "typeVersion": 2, | |
| "position": [ | |
| -2544, | |
| 224 | |
| ], | |
| "id": "0f5bcd37-894d-476a-a148-44f3d21ebc86" | |
| }, | |
| { | |
| "parameters": { | |
| "respondWith": "text", | |
| "responseBody": "={{ $json[\"html_content\"] }}", | |
| "options": {} | |
| }, | |
| "name": "Response with HTML", | |
| "type": "n8n-nodes-base.respondToWebhook", | |
| "typeVersion": 1, | |
| "position": [ | |
| -1728, | |
| 224 | |
| ], | |
| "id": "4c7bbd6b-aeb2-44af-a910-e7018e822551" | |
| }, | |
| { | |
| "parameters": { | |
| "updates": [ | |
| "message" | |
| ], | |
| "additionalFields": {} | |
| }, | |
| "type": "n8n-nodes-base.telegramTrigger", | |
| "typeVersion": 1.2, | |
| "position": [ | |
| -2768, | |
| -704 | |
| ], | |
| "id": "2730c018-093d-4d18-8332-600dda1f0354", | |
| "name": "Telegram Trigger", | |
| "webhookId": "7a1949da-594b-4f86-825a-7ca277efad50", | |
| "credentials": { | |
| "telegramApi": { | |
| "id": "sTNb3pmq5jE43YMZ", | |
| "name": "Telegram account" | |
| } | |
| } | |
| } | |
| ], | |
| "pinData": {}, | |
| "connections": { | |
| "Switch": { | |
| "main": [ | |
| [ | |
| { | |
| "node": "Set Message from Text", | |
| "type": "main", | |
| "index": 0 | |
| } | |
| ], | |
| [ | |
| { | |
| "node": "Get audio", | |
| "type": "main", | |
| "index": 0 | |
| } | |
| ], | |
| [ | |
| { | |
| "node": "If", | |
| "type": "main", | |
| "index": 0 | |
| } | |
| ] | |
| ] | |
| }, | |
| "OpenAI": { | |
| "main": [ | |
| [ | |
| { | |
| "node": "Set Message from Audio", | |
| "type": "main", | |
| "index": 0 | |
| } | |
| ] | |
| ] | |
| }, | |
| "Message a model": { | |
| "main": [ | |
| [ | |
| { | |
| "node": "Parse AI JSON", | |
| "type": "main", | |
| "index": 0 | |
| } | |
| ] | |
| ] | |
| }, | |
| "Set Message from Audio": { | |
| "main": [ | |
| [ | |
| { | |
| "node": "Message a model", | |
| "type": "main", | |
| "index": 0 | |
| } | |
| ] | |
| ] | |
| }, | |
| "Set Message from Text": { | |
| "main": [ | |
| [ | |
| { | |
| "node": "Message a model", | |
| "type": "main", | |
| "index": 0 | |
| } | |
| ] | |
| ] | |
| }, | |
| "Parse AI JSON": { | |
| "main": [ | |
| [ | |
| { | |
| "node": "Has transactions?", | |
| "type": "main", | |
| "index": 0 | |
| } | |
| ] | |
| ] | |
| }, | |
| "Has transactions?": { | |
| "main": [ | |
| [ | |
| { | |
| "node": "Split Out", | |
| "type": "main", | |
| "index": 0 | |
| } | |
| ], | |
| [ | |
| { | |
| "node": "Fail message", | |
| "type": "main", | |
| "index": 0 | |
| } | |
| ] | |
| ] | |
| }, | |
| "Fail message": { | |
| "main": [ | |
| [ | |
| { | |
| "node": "Send message", | |
| "type": "main", | |
| "index": 0 | |
| } | |
| ] | |
| ] | |
| }, | |
| "Sucess message": { | |
| "main": [ | |
| [ | |
| { | |
| "node": "Loop Over Items", | |
| "type": "main", | |
| "index": 0 | |
| } | |
| ] | |
| ] | |
| }, | |
| "Save": { | |
| "main": [ | |
| [ | |
| { | |
| "node": "Sucess message", | |
| "type": "main", | |
| "index": 0 | |
| } | |
| ] | |
| ] | |
| }, | |
| "Split Out": { | |
| "main": [ | |
| [ | |
| { | |
| "node": "Loop Over Items", | |
| "type": "main", | |
| "index": 0 | |
| } | |
| ] | |
| ] | |
| }, | |
| "Edit Fields": { | |
| "main": [ | |
| [ | |
| { | |
| "node": "Save", | |
| "type": "main", | |
| "index": 0 | |
| } | |
| ] | |
| ] | |
| }, | |
| "Loop Over Items": { | |
| "main": [ | |
| [ | |
| { | |
| "node": "Aggregate", | |
| "type": "main", | |
| "index": 0 | |
| } | |
| ], | |
| [ | |
| { | |
| "node": "Edit Fields", | |
| "type": "main", | |
| "index": 0 | |
| } | |
| ] | |
| ] | |
| }, | |
| "If": { | |
| "main": [ | |
| [ | |
| { | |
| "node": "Get CSV", | |
| "type": "main", | |
| "index": 0 | |
| } | |
| ] | |
| ] | |
| }, | |
| "Get CSV": { | |
| "main": [ | |
| [ | |
| { | |
| "node": "Convert To Spreadsheet", | |
| "type": "main", | |
| "index": 0 | |
| } | |
| ] | |
| ] | |
| }, | |
| "Get audio": { | |
| "main": [ | |
| [ | |
| { | |
| "node": "OpenAI", | |
| "type": "main", | |
| "index": 0 | |
| } | |
| ] | |
| ] | |
| }, | |
| "Convert To Spreadsheet": { | |
| "main": [ | |
| [ | |
| { | |
| "node": "Code in JavaScript", | |
| "type": "main", | |
| "index": 0 | |
| } | |
| ] | |
| ] | |
| }, | |
| "Code in JavaScript": { | |
| "main": [ | |
| [ | |
| { | |
| "node": "Message a model", | |
| "type": "main", | |
| "index": 0 | |
| } | |
| ] | |
| ] | |
| }, | |
| "Map Chat": { | |
| "main": [ | |
| [ | |
| { | |
| "node": "Switch", | |
| "type": "main", | |
| "index": 0 | |
| } | |
| ] | |
| ] | |
| }, | |
| "Aggregate": { | |
| "main": [ | |
| [ | |
| { | |
| "node": "Send message", | |
| "type": "main", | |
| "index": 0 | |
| } | |
| ] | |
| ] | |
| }, | |
| "Webhook": { | |
| "main": [ | |
| [ | |
| { | |
| "node": "Set Infos", | |
| "type": "main", | |
| "index": 0 | |
| } | |
| ] | |
| ] | |
| }, | |
| "List Finance (Data Table)": { | |
| "main": [ | |
| [ | |
| { | |
| "node": "Generate HTML", | |
| "type": "main", | |
| "index": 0 | |
| } | |
| ] | |
| ] | |
| }, | |
| "Generate HTML": { | |
| "main": [ | |
| [ | |
| { | |
| "node": "Response with HTML", | |
| "type": "main", | |
| "index": 0 | |
| } | |
| ] | |
| ] | |
| }, | |
| "Set Infos": { | |
| "main": [ | |
| [ | |
| { | |
| "node": "List Finance (Data Table)", | |
| "type": "main", | |
| "index": 0 | |
| } | |
| ] | |
| ] | |
| }, | |
| "Telegram Trigger": { | |
| "main": [ | |
| [ | |
| { | |
| "node": "Map Chat", | |
| "type": "main", | |
| "index": 0 | |
| } | |
| ] | |
| ] | |
| } | |
| }, | |
| "active": true, | |
| "settings": { | |
| "executionOrder": "v1" | |
| }, | |
| "versionId": "3f5ea76e-6c0d-4c4c-856b-b776c00eae32", | |
| "meta": { | |
| "templateCredsSetupCompleted": true, | |
| "instanceId": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" | |
| }, | |
| "id": "N1cHhvnFycILn5LP", | |
| "tags": [] | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment