name: comercialrs-crm-view-union-pattern
description: Create a Supabase view that unions an external CRM table (crm_leads_duplicate) with a local table (sales WHERE source=manual) — replacing a redundant sync layer without breaking existing frontend operations.
ComercialRS CRM-to-View Migration Pattern
Quando usar
Quando o frontend precisa ler de uma tabela que é apenas espelho de outra (crm_leads_duplicate → sales), e você quer pular a camada de sync duplicada substituindo por uma view UNION, sem quebrar operações existentes (SELECT, UPDATE, INSERT, DELETE).
Problema original
- -
crm_leads_duplicate= tabela fonte do CRM (trigger externo alimenta) - -
crm-lead-sync(Edge Function) = copia decrm_leads_duplicate→sales(camada redundante) - -
sales= tabela que o frontend lia - - Frontend precisa operar: SELECT, UPDATE, INSERT, DELETE
- - Usuário não quer quebrar integrações existentes
- - Duplicados em
salespara CRM: mesmoexternal_refaparece N vezes na tabelasales(vários syncs do mesmo lead). Solução: subquery comDISTINCT ON (external_ref)ordenando porupdated_at DESC. - -
Fechamentoé NULL ou texto vazio: comparisonc."Fechamento" <> ''pode falhar se a coluna fortimestamptz. Usarc."Fechamento" IS NOT NULL. - -
source='manual'retornando vazio: se não há registros comsource='manual'no banco, a UNION retorna nada para manual. Verificar comSELECT source, COUNT(*) FROM sales_crm_view GROUP BY source. - - Supabase Management API bloqueada: Cloudflare 1010 em
api.supabase.com/v1/projects/PROJECT/database/query. Alternativa:supabase db query --linkedvia CLI local (funciona do VPS). - - Prefixo de colunas com espaços: colunas como
"Produto.Nome"no CRM precisam de aspas duplas sempre no SQL.
Solução: View UNION
1. Criar view no banco
`sql
CREATE OR REPLACE VIEW public.sales_crm_view AS
-- CRM: crm_leads_duplicate como fonte, LEFT JOIN sales para sync meta (notes, etc)
SELECT
(CASE WHEN s.id IS NOT NULL THEN s.id::text ELSE 'crm-' || c.id_internal END) AS id,
('crm-lead-' || c."Id") AS external_ref,
c."Nome" AS title,
COALESCE(c."Vendedor", '') AS seller,
NULL AS seller_email,
COALESCE(c."Contrato", '') AS policy_number,
COALESCE(c."Produto.Nome", '') AS product,
COALESCE(c."Valor" :: numeric, 0) AS value,
CASE
WHEN c."Etapa" IN ('Novos Leads', 'Qualificação', 'Reunião agendada')
THEN 'emissao'
WHEN c."Etapa" IN ('Remarketing ', 'Analise da Seguradora')
THEN 'analise_seguradora'
WHEN c."Etapa" = 'Proposta Enviada p/cliente'
THEN 'aguardando_pagamento'
WHEN c."Etapa" IN ('Negócios Fechado', 'Fechado')
THEN 'pago'
WHEN c."Etapa" IN ('Sem Interresse ', 'Declinado')
THEN 'declinado'
ELSE 'emissao'
END AS status,
'crm' AS source,
CASE
WHEN c."Fechamento" IS NOT NULL
THEN c."Fechamento"
WHEN c."Etapa" IN ('Negócios Fechado', 'Fechado')
THEN c."CriadoEm"
ELSE NULL
END AS paid_at,
c."CriadoEm" AS created_at,
c."CriadoEm" AS emitted_at,
s.synced_at,
s.notes,
s.link,
s.deleted_at,
c."Id" AS crm_id,
c.id_internal AS crm_internal_id,
s.id AS sales_id
FROM crm_leads_duplicate c
LEFT JOIN (
-- Deduplica: para cada external_ref, pega só o registro mais recente
SELECT DISTINCT ON (external_ref) *
FROM sales
WHERE source = 'crm' AND deleted_at IS NULL
ORDER BY external_ref, updated_at DESC NULLS LAST
) s ON s.external_ref = ('crm-lead-' || c."Id")
UNION ALL
SELECT
s.id::text,
COALESCE(s.external_ref, ''),
s.title,
COALESCE(s.seller, ''),
s.seller_email,
COALESCE(s.policy_number, ''),
COALESCE(s.product, ''),
s.value,
s.status,
s.source,
s.paid_at,
s.created_at,
s.emitted_at,
s.synced_at,
s.notes,
s.link,
s.deleted_at,
NULL AS crm_id,
NULL AS crm_internal_id,
s.id AS sales_id
FROM sales s
WHERE s.source = 'manual' AND s.deleted_at IS NULL;
`
2. Padrão de campos críticos
| Campo | Propósito |
| ------- | ----------- |
id | Pode colidir entre CRM e manual — usar prefixo (crm- + id_internal) quando sem JOIN |
sales_id | UUID real da tabela sales — necessário para UPDATE/DELETE nas operações |
crm_id | ID do registro no sistema externo (CRM Painel do Corretor) |
source | Distingue 'crm' vs 'manual' — usado para bloquear edição/exclusão |
3. Frontend: bloquear edição de CRM
`typescript
// SaleDetailDialog — handleSave
if (sale.source === 'crm') {
toast.error('Vendas do CRM são somente leitura. Edite diretamente no Painel do Corretor.');
return;
}
// handleDelete
if (sale.source === 'crm') {
toast.error('Vendas do CRM não podem ser excluídas aqui.');
return;
}
// handleSaveNotes — usa salesId se disponível
const targetId = sale.salesId || sale.id;
`
4. pitfalls encontrados
Verificação pós-deploy
`sql
-- Contagem por source
SELECT source, COUNT(*) FROM sales_crm_view GROUP BY source;
-- Verificar duplicados CRM
SELECT "Id", COUNT() as cnt FROM crm_leads_duplicate GROUP BY "Id" HAVING COUNT() > 1;
-- Verificar CRM sem matching em sales
SELECT s.id, s.external_ref FROM sales s
WHERE s.source = 'crm' AND s.deleted_at IS NULL
AND NOT EXISTS (SELECT 1 FROM crm_leads_duplicate c WHERE 'crm-lead-' || c."Id" = s.external_ref);
`