diff --git a/supabase-sheet-vendas/INSTALL.md b/supabase-sheet-vendas/INSTALL.md new file mode 100644 index 0000000..3d608a9 --- /dev/null +++ b/supabase-sheet-vendas/INSTALL.md @@ -0,0 +1,50 @@ +# Supabase → Sheet VENDAS + +## O que faz +Busca `fechamentos_web` do Supabase e faz **append** na sheet VENDAS, incremental por `atualizado_em` UTC. + +## Pré-requisitos +1. **Supabase account 2 (self-hosted)** — credencial n8n com service role JWT +2. **Google Sheets account** — OAuth com acesso ao spreadsheet `1RGa7Qigu5kWJGCOx3kJ3Oa8wTTJeD-CzGjBYazrjsWk` +3. **Sheet "Sincronizacao"** no spreadsheet — célula A1 com o UTC timestamp do último sync (para o primeiro run, deixar vazio ou usar `1970-01-01T00:00:00Z`) + +## Mapa de colunas + +| Sheet Col | Supabase | +|-----------|----------| +| DATA | `data` | +| EMPRESA | `loja` → `Aliança`/`União` | +| SANGRIA | `total_sangrias` | +| DINHEIRO | `fechamento_dinheiro` | +| PIX_TAXA | `total_pixcnpj` | +| PIX_LOJA | `saldo_pixcnpj` | +| PRAZO | `total_areceber` | +| CREDITO | `total_cartao_credito` | +| DEBITO | `total_cartao_debito` | +| VOUCHER | `total_cartao_alimentacao` | +| CARTAO | `fechamento_cartoes` | +| VALES | `total_vales` | +| DESPESAS | `total_despesas` | +| CANCELADOS | `total_cancelamentos` | +| DIFERENCA | `diferenca` | +| CAIXA | `operador` | +| PG | `status_diferenca === 'ok'` | +| T_VENDA | `0` (fórmula na Sheet) | +| TX_CAIXA | `0` (fórmula na Sheet) | +| SISTEMA | `0` (fórmula na Sheet) | +| CLIENTES | `0` (não existe em fechamentos_web) | + +## Como instalar + +1. Exportar workflow: `Workflows → Add workflow → ⋮ → Import from Code` +2. Copiar o conteúdo de `workflow.js` +3. Configurar credenciais: + - Node `Fetch from Supabase` → `Supabase account 2 (self-hosted)` + - Nodes `Read Last Sync` / `Append to VENDAS` / `Update Last Sync` → `Google Sheets account` +4. Criar aba `Sincronizacao` no spreadsheet com célula A1 vazia (primeiro sync vai buscar tudo) +5. Activar workflow + +## Notas +- **Schedule**: `0 20 * * *` (20h BRT = 23h UTC) +- O nó `Filter + Map` detecta se há linhas novas; se não houver, faz update do sync marker sem append +- Se o Kong do Supabase mudar de IP (erro 502), corrigir em `/opt/supabase/docker/volumes/api/kong.yml` diff --git a/supabase-sheet-vendas/workflow.js b/supabase-sheet-vendas/workflow.js new file mode 100644 index 0000000..31f2ee2 --- /dev/null +++ b/supabase-sheet-vendas/workflow.js @@ -0,0 +1,187 @@ +import { workflow, trigger, node } from 'n8n-workflow-sdk'; + +export default workflow('Supabase → Sheet VENDAS', { + description: 'Busca fechamentos_web do Supabase e append na Sheet VENDAS (incremental por atualizado_em UTC)', + + nodes: [ + // ── TRIGGER: 20h BRT (23h UTC) ────────────────────────────────── + trigger('Schedule Daily 20h', { + type: 'n8n-nodes-base.scheduleTrigger', + typeVersion: 1.2, + position: [100, 300], + parameters: { + rule: { interval: [{ field: 'cron', expression: '0 20 * * *' }] } + } + }), + + // ── READ LAST SYNC from Sheet Sincronizacao A1 ─────────────────── + node('Read Last Sync', { + type: 'n8n-nodes-base.googleSheets', + typeVersion: 4.2, + position: [340, 300], + credentials: { googleSheetsOAuth2Api: { id: 'qVVlfm46BZ6tX5VX', name: 'Google Sheets account' } }, + parameters: { + operation: 'read', + documentId: { __rl: true, mode: 'id', value: '1RGa7Qigu5kWJGCOx3kJ3Oa8wTTJeD-CzGjBYazrjsWk' }, + sheetName: { __rl: true, mode: 'list', value: 'Sincronizacao' }, + cellSelection: { __rl: true, mode: 'list', value: 'A1' }, + options: { cellFormat: 'USER_ENTERED' } + } + }), + + // ── FETCH fechamentos_web from Supabase ────────────────────────── + node('Fetch from Supabase', { + type: 'n8n-nodes-base.httpRequest', + typeVersion: 4.2, + position: [600, 300], + credentials: { supabaseApi: { id: 'Tw9r35a9Iz3P9q7x', name: 'Supabase account 2 (self-hosted)' } }, + parameters: { + method: 'GET', + url: '=https://supabase.ofrangao.com.br/rest/v1/fechamentos_web', + queryParameters: { + parameters: [ + { + name: 'select', + value: '=id_fechamento,data,loja,turno,operador,fechamento_dinheiro,fechamento_cartoes,total_sangrias,total_despesas,total_vales,total_pixcnpj,saldo_pixcnpj,total_areceber,total_cartao_credito,total_cartao_debito,total_cartao_alimentacao,total_cancelamentos,diferenca,status_diferenca,atualizado_em' + }, + { name: 'order', value: '=atualizado_em.asc' }, + { name: 'limit', value: '=500' } + ] + } + } + }), + + // ── CODE: Filter by lastSync + Map to Sheet columns ───────────── + node('Filter + Map', { + type: 'n8n-nodes-base.code', + typeVersion: 2, + position: [860, 300], + parameters: { + jsCode: ` +// 1. lastSync da Sheet +const lastSyncRaw = $input.first().json.value || null; +const lastSync = lastSyncRaw ? new Date(lastSyncRaw).toISOString() : null; + +// 2. Todas as rows do Supabase +const allRows = $input.all().map(item => item.json); + +// 3. Filtrar por lastSync +let filtered = lastSync ? allRows.filter(r => r.atualizado_em && r.atualizado_em > lastSync) : allRows; + +if (filtered.length === 0) { + // Sem linhas novas —marker o sync point com lastSync antigo para não re-buscar + return [{ + json: { + _action: 'noop', + _lastSync: lastSync, + _max_updated: lastSync, + DATA:'', EMPRESA:'', SANGRIA:0, DESPESAS:0, VALES:0, + DINHEIRO:0, PIX_TAXA:0, PIX_LOJA:0, PRAZO:0, P2:0, + CREDITO:0, DEBITO:0, VOUCHER:0, CARTAO:0, + T_VENDA:0, TX_CAIXA:0, CLIENTES:0, CANCELADOS:0, + SISTEMA:0, DIFERENCA:0, CAIXA:'', PG:false + } + }]; +} + +// 4. MAX(atualizado_em) do batch +const maxUpdated = filtered.reduce((m, r) => + r.atualizado_em > m ? r.atualizado_em : m, filtered[0].atualizado_em); + +// 5. Mapear para colunas Sheet +const LOJA_MAP = { alianca: 'Aliança', uniao: 'União' }; + +return filtered.map(r => ({ + json: { + DATA: r.data, + EMPRESA: LOJA_MAP[r.loja] || r.loja, + SANGRIA: r.total_sangrias || 0, + DESPESAS: r.total_despesas || 0, + VALES: r.total_vales || 0, + DINHEIRO: r.fechamento_dinheiro || 0, + PIX_TAXA: r.total_pixcnpj || 0, + PIX_LOJA: r.saldo_pixcnpj || 0, + PRAZO: r.total_areceber || 0, + P2: 0, + CREDITO: r.total_cartao_credito || 0, + DEBITO: r.total_cartao_debito || 0, + VOUCHER: r.total_cartao_alimentacao || 0, + CARTAO: r.fechamento_cartoes || 0, + T_VENDA: 0, // fórmula na Sheet + TX_CAIXA: 0, // fórmula na Sheet + CLIENTES: 0, + CANCELADOS: r.total_cancelamentos || 0, + SISTEMA: 0, // fórmula na Sheet + DIFERENCA: r.diferenca || 0, + CAIXA: r.operador || '', + PG: r.status_diferenca === 'ok', + _action: 'append', + _max_updated: maxUpdated + } +})); +` + } + }), + + // ── APPEND to Sheet VENDAS ────────────────────────────────────── + node('Append to VENDAS', { + type: 'n8n-nodes-base.googleSheets', + typeVersion: 4.2, + position: [1100, 200], + credentials: { googleSheetsOAuth2Api: { id: 'qVVlfm46BZ6tX5VX', name: 'Google Sheets account' } }, + parameters: { + operation: 'append', + documentId: { __rl: true, mode: 'id', value: '1RGa7Qigu5kWJGCOx3kJ3Oa8wTTJeD-CzGjBYazrjsWk' }, + sheetName: { __rl: true, mode: 'list', value: 'VENDAS' }, + options: { cellFormat: 'USER_ENTERED' } + } + }), + + // ── UPDATE LAST SYNC in Sincronizacao A1 ─────────────────────── + // Recebe o primeiro item (com _max_updated) de Filter+Map + node('Update Last Sync', { + type: 'n8n-nodes-base.googleSheets', + typeVersion: 4.2, + position: [1100, 440], + credentials: { googleSheetsOAuth2Api: { id: 'qVVlfm46BZ6tX5VX', name: 'Google Sheets account' } }, + parameters: { + operation: 'update', + documentId: { __rl: true, mode: 'id', value: '1RGa7Qigu5kWJGCOx3kJ3Oa8wTTJeD-CzGjBYazrjsWk' }, + sheetName: { __rl: true, mode: 'list', value: 'Sincronizacao' }, + cellSelection: { __rl: true, mode: 'list', value: 'A1' }, + valueInputOption: 'USER_ENTERED', + options: { cellFormat: 'USER_ENTERED' } + } + }), + ], + + connections: { + 'Schedule Daily 20h': { + main: [[{ node: 'Read Last Sync', type: 'main', index: 0 }]] + }, + 'Read Last Sync': { + main: [[{ node: 'Fetch from Supabase', type: 'main', index: 0 }]] + }, + 'Fetch from Supabase': { + main: [[{ node: 'Filter + Map', type: 'main', index: 0 }]] + }, + 'Filter + Map': { + main: [ + // Branch 0: dados para Sheet + [{ node: 'Append to VENDAS', type: 'main', index: 0 }], + // Branch 1: marker sync (vai directo para Update Last Sync) + [{ node: 'Update Last Sync', type: 'main', index: 0 }] + ] + }, + 'Append to VENDAS': { + main: [[{ node: 'Update Last Sync', type: 'main', index: 0 }]] + } + }, + + settings: { + executionOrder: 'v1', + saveExecutionProgress: true, + saveDataSuccessExecution: 'all', + saveDataErrorExecution: 'all' + } +});